417 lines
16 KiB
Go
417 lines
16 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"wucher/internal/domain/base"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
type BaseWriteError struct {
|
|
Status int
|
|
Title string
|
|
Detail string
|
|
Pointer string
|
|
}
|
|
|
|
func (e *BaseWriteError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
return e.Detail
|
|
}
|
|
|
|
type BaseWriteResult struct {
|
|
Row *base.Base
|
|
CategoryType string
|
|
PreviousFotoAttachmentID []byte
|
|
}
|
|
|
|
type BaseOperationalShiftTimeInput struct {
|
|
DateStart time.Time
|
|
DateEnd time.Time
|
|
StartTimeType string
|
|
EndTimeType string
|
|
ShiftStart *time.Time
|
|
ShiftEnd *time.Time
|
|
}
|
|
|
|
type BaseCreateInput struct {
|
|
ID []byte
|
|
CategoryType string
|
|
BaseName string
|
|
BaseAbbreviation string
|
|
Address string
|
|
Latitude float64
|
|
Longitude float64
|
|
LandlineNumber string
|
|
MobileNumber string
|
|
Email string
|
|
SortKey *int
|
|
SMSAlert bool
|
|
Checklist bool
|
|
LegTime string
|
|
DefaultStartTimeType string
|
|
DefaultEndTimeType string
|
|
DefaultShiftStart *time.Time
|
|
DefaultShiftEnd *time.Time
|
|
UTC string
|
|
Dry bool
|
|
ControlCenter bool
|
|
DUL bool
|
|
Notes string
|
|
HEMSEDCContactIDs [][]byte
|
|
MedPaxContactIDs [][]byte
|
|
ResponsiblePilotContactIDs [][]byte
|
|
OperationalShiftTimes []BaseOperationalShiftTimeInput
|
|
IsActive bool
|
|
CreatedBy []byte
|
|
UpdatedBy []byte
|
|
FotoAttachmentID []byte
|
|
}
|
|
|
|
type BaseUpdateInput struct {
|
|
ID []byte
|
|
CategoryType string
|
|
BaseName *string
|
|
BaseAbbreviation *string
|
|
Address *string
|
|
Latitude *float64
|
|
Longitude *float64
|
|
LandlineNumber *string
|
|
MobileNumber *string
|
|
Email *string
|
|
SortKeySet bool
|
|
SortKeyValid bool
|
|
SortKeyValue *int
|
|
SMSAlert *bool
|
|
Checklist *bool
|
|
LegTime *string
|
|
DefaultStartTimeType *string
|
|
DefaultEndTimeType *string
|
|
DefaultShiftStart *time.Time
|
|
DefaultShiftEnd *time.Time
|
|
UTC *string
|
|
Dry *bool
|
|
ControlCenter *bool
|
|
DUL *bool
|
|
Notes *string
|
|
HEMSEDCContactIDs [][]byte
|
|
HEMSEDCContactIDsSet bool
|
|
MedPaxContactIDs [][]byte
|
|
MedPaxContactIDsSet bool
|
|
ResponsiblePilotContactIDs [][]byte
|
|
ResponsiblePilotContactIDsSet bool
|
|
OperationalShiftTimes []BaseOperationalShiftTimeInput
|
|
OperationalShiftTimesSet bool
|
|
IsActive *bool
|
|
CreatedBy []byte
|
|
CreatedBySet bool
|
|
UpdatedBy []byte
|
|
UpdatedBySet bool
|
|
FotoAttachmentID []byte
|
|
FotoAttachmentSet bool
|
|
}
|
|
|
|
func (s *BaseService) CreateDetailed(ctx context.Context, in BaseCreateInput) (*BaseWriteResult, error) {
|
|
if s == nil || s.repo == nil {
|
|
return nil, &BaseWriteError{Status: http.StatusInternalServerError, Title: "Create failed", Detail: "base service is not configured"}
|
|
}
|
|
category, ok := base.NormalizeCategoryType(in.CategoryType)
|
|
if !ok {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "invalid base category type", Pointer: "/data/attributes/base_category"}
|
|
}
|
|
baseName := strings.TrimSpace(in.BaseName)
|
|
if baseName == "" {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "base is required", Pointer: "/data/attributes/base"}
|
|
}
|
|
if in.Latitude < -90 || in.Latitude > 90 {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "latitude must be between -90 and 90", Pointer: "/data/attributes/latitude"}
|
|
}
|
|
if in.Longitude < -180 || in.Longitude > 180 {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "longitude must be between -180 and 180", Pointer: "/data/attributes/longitude"}
|
|
}
|
|
defaultStartTimeType := normalizeShiftTimeType(in.DefaultStartTimeType)
|
|
defaultEndTimeType := normalizeShiftTimeType(in.DefaultEndTimeType)
|
|
if defaultStartTimeType == base.ShiftTimeTypeFixed && in.DefaultShiftStart == nil {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_start is required", Pointer: "/data/attributes/default_shift_start"}
|
|
}
|
|
if defaultEndTimeType == base.ShiftTimeTypeFixed && in.DefaultShiftEnd == nil {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_end is required", Pointer: "/data/attributes/default_shift_end"}
|
|
}
|
|
|
|
row := &base.Base{
|
|
ID: append([]byte(nil), in.ID...),
|
|
BaseName: baseName,
|
|
BaseAbbreviation: strings.TrimSpace(in.BaseAbbreviation),
|
|
Address: strings.TrimSpace(in.Address),
|
|
Latitude: in.Latitude,
|
|
Longitude: in.Longitude,
|
|
LandlineNumber: strings.TrimSpace(in.LandlineNumber),
|
|
MobileNumber: strings.TrimSpace(in.MobileNumber),
|
|
Email: strings.TrimSpace(in.Email),
|
|
SortKey: cloneIntPtr(in.SortKey),
|
|
SMSAlert: in.SMSAlert,
|
|
Checklist: in.Checklist,
|
|
LegTime: strings.TrimSpace(in.LegTime),
|
|
DefaultStartTimeType: defaultStartTimeType,
|
|
DefaultEndTimeType: defaultEndTimeType,
|
|
DefaultShiftStart: timePtrToClockString(in.DefaultShiftStart),
|
|
DefaultShiftEnd: timePtrToClockString(in.DefaultShiftEnd),
|
|
UTC: normalizeUTCString(in.UTC),
|
|
Dry: in.Dry,
|
|
ControlCenter: in.ControlCenter,
|
|
DUL: in.DUL,
|
|
Notes: strings.TrimSpace(in.Notes),
|
|
IsActive: in.IsActive,
|
|
HEMSEDCContactIDs: appendUniqueIDs(in.HEMSEDCContactIDs),
|
|
MedPaxContactIDs: appendUniqueIDs(in.MedPaxContactIDs),
|
|
ResponsiblePilotContactIDs: appendUniqueIDs(in.ResponsiblePilotContactIDs),
|
|
OperationalShiftTimes: cloneOperationalShiftTimeInputs(in.OperationalShiftTimes),
|
|
CreatedBy: append([]byte(nil), in.CreatedBy...),
|
|
UpdatedBy: append([]byte(nil), in.UpdatedBy...),
|
|
FotoAttachmentID: append([]byte(nil), in.FotoAttachmentID...),
|
|
}
|
|
if len(row.ID) == 0 {
|
|
row.ID = uuidv7.MustBytes()
|
|
}
|
|
if err := s.CreateBase(ctx, row, category); err != nil {
|
|
return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()}
|
|
}
|
|
created, err := s.GetBaseByID(ctx, row.ID, category)
|
|
if err != nil {
|
|
return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()}
|
|
}
|
|
if created == nil {
|
|
created = row
|
|
}
|
|
return &BaseWriteResult{Row: created, CategoryType: category}, nil
|
|
}
|
|
|
|
func (s *BaseService) UpdateDetailed(ctx context.Context, in BaseUpdateInput) (*BaseWriteResult, error) {
|
|
if s == nil || s.repo == nil {
|
|
return nil, &BaseWriteError{Status: http.StatusInternalServerError, Title: "Update failed", Detail: "base service is not configured"}
|
|
}
|
|
category, ok := base.NormalizeCategoryType(in.CategoryType)
|
|
if !ok {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "invalid base category type", Pointer: "/data/attributes/base_category"}
|
|
}
|
|
if len(in.ID) != 16 {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "uuid is invalid UUID", Pointer: "/path/uuid"}
|
|
}
|
|
|
|
existing, err := s.GetBaseByID(ctx, in.ID, category)
|
|
if err != nil {
|
|
return nil, &BaseWriteError{Status: http.StatusNotFound, Title: "Not found", Detail: categoryNotFoundDetail(category)}
|
|
}
|
|
if existing == nil {
|
|
return nil, &BaseWriteError{Status: http.StatusNotFound, Title: "Not found", Detail: categoryNotFoundDetail(category)}
|
|
}
|
|
prevFotoAttachmentID := append([]byte(nil), existing.FotoAttachmentID...)
|
|
if in.Latitude != nil && (*in.Latitude < -90 || *in.Latitude > 90) {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "latitude must be between -90 and 90", Pointer: "/data/attributes/latitude"}
|
|
}
|
|
if in.Longitude != nil && (*in.Longitude < -180 || *in.Longitude > 180) {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "longitude must be between -180 and 180", Pointer: "/data/attributes/longitude"}
|
|
}
|
|
|
|
if in.BaseName != nil {
|
|
v := strings.TrimSpace(*in.BaseName)
|
|
if v == "" {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "base cannot be empty", Pointer: "/data/attributes/base"}
|
|
}
|
|
existing.BaseName = v
|
|
}
|
|
if in.BaseAbbreviation != nil {
|
|
existing.BaseAbbreviation = strings.TrimSpace(*in.BaseAbbreviation)
|
|
}
|
|
if in.Address != nil {
|
|
existing.Address = strings.TrimSpace(*in.Address)
|
|
}
|
|
if in.Latitude != nil {
|
|
existing.Latitude = *in.Latitude
|
|
}
|
|
if in.Longitude != nil {
|
|
existing.Longitude = *in.Longitude
|
|
}
|
|
if in.LandlineNumber != nil {
|
|
existing.LandlineNumber = strings.TrimSpace(*in.LandlineNumber)
|
|
}
|
|
if in.MobileNumber != nil {
|
|
existing.MobileNumber = strings.TrimSpace(*in.MobileNumber)
|
|
}
|
|
if in.Email != nil {
|
|
existing.Email = strings.TrimSpace(*in.Email)
|
|
}
|
|
if in.SortKeySet {
|
|
if in.SortKeyValid {
|
|
existing.SortKey = cloneIntPtr(in.SortKeyValue)
|
|
} else {
|
|
existing.SortKey = nil
|
|
}
|
|
}
|
|
if in.SMSAlert != nil {
|
|
existing.SMSAlert = *in.SMSAlert
|
|
}
|
|
if in.Checklist != nil {
|
|
existing.Checklist = *in.Checklist
|
|
}
|
|
if in.LegTime != nil {
|
|
existing.LegTime = strings.TrimSpace(*in.LegTime)
|
|
}
|
|
if in.DefaultStartTimeType != nil {
|
|
existing.DefaultStartTimeType = normalizeShiftTimeType(*in.DefaultStartTimeType)
|
|
}
|
|
if in.DefaultEndTimeType != nil {
|
|
existing.DefaultEndTimeType = normalizeShiftTimeType(*in.DefaultEndTimeType)
|
|
}
|
|
if in.DefaultShiftStart != nil {
|
|
existing.DefaultShiftStart = timePtrToClockString(in.DefaultShiftStart)
|
|
}
|
|
if in.DefaultShiftEnd != nil {
|
|
existing.DefaultShiftEnd = timePtrToClockString(in.DefaultShiftEnd)
|
|
}
|
|
if normalizeShiftTimeType(existing.DefaultStartTimeType) != base.ShiftTimeTypeFixed {
|
|
existing.DefaultShiftStart = ""
|
|
}
|
|
if normalizeShiftTimeType(existing.DefaultEndTimeType) != base.ShiftTimeTypeFixed {
|
|
existing.DefaultShiftEnd = ""
|
|
}
|
|
if normalizeShiftTimeType(existing.DefaultStartTimeType) == base.ShiftTimeTypeFixed && strings.TrimSpace(existing.DefaultShiftStart) == "" {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_start is required", Pointer: "/data/attributes/default_shift_start"}
|
|
}
|
|
if normalizeShiftTimeType(existing.DefaultEndTimeType) == base.ShiftTimeTypeFixed && strings.TrimSpace(existing.DefaultShiftEnd) == "" {
|
|
return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_end is required", Pointer: "/data/attributes/default_shift_end"}
|
|
}
|
|
if in.UTC != nil {
|
|
existing.UTC = normalizeUTCString(*in.UTC)
|
|
}
|
|
if in.Dry != nil {
|
|
existing.Dry = *in.Dry
|
|
}
|
|
if in.ControlCenter != nil {
|
|
existing.ControlCenter = *in.ControlCenter
|
|
}
|
|
if in.DUL != nil {
|
|
existing.DUL = *in.DUL
|
|
}
|
|
if in.Notes != nil {
|
|
existing.Notes = strings.TrimSpace(*in.Notes)
|
|
}
|
|
if in.HEMSEDCContactIDsSet {
|
|
existing.HEMSEDCContactIDs = appendUniqueIDs(in.HEMSEDCContactIDs)
|
|
}
|
|
if in.MedPaxContactIDsSet {
|
|
existing.MedPaxContactIDs = appendUniqueIDs(in.MedPaxContactIDs)
|
|
}
|
|
if in.ResponsiblePilotContactIDsSet {
|
|
existing.ResponsiblePilotContactIDs = appendUniqueIDs(in.ResponsiblePilotContactIDs)
|
|
}
|
|
if in.OperationalShiftTimesSet {
|
|
existing.OperationalShiftTimes = cloneOperationalShiftTimeInputs(in.OperationalShiftTimes)
|
|
if existing.OperationalShiftTimes == nil {
|
|
existing.OperationalShiftTimes = []base.BaseOperationalShiftTime{}
|
|
}
|
|
} else {
|
|
existing.OperationalShiftTimes = nil
|
|
}
|
|
if in.CreatedBySet {
|
|
existing.CreatedBy = append([]byte(nil), in.CreatedBy...)
|
|
}
|
|
if in.UpdatedBySet {
|
|
existing.UpdatedBy = append([]byte(nil), in.UpdatedBy...)
|
|
}
|
|
if in.FotoAttachmentSet {
|
|
existing.FotoAttachmentID = append([]byte(nil), in.FotoAttachmentID...)
|
|
}
|
|
if in.IsActive != nil {
|
|
existing.IsActive = *in.IsActive
|
|
}
|
|
|
|
if err := s.UpdateBase(ctx, existing, category); err != nil {
|
|
return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()}
|
|
}
|
|
updated, err := s.GetBaseByID(ctx, existing.ID, category)
|
|
if err != nil {
|
|
return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()}
|
|
}
|
|
if updated == nil {
|
|
updated = existing
|
|
}
|
|
return &BaseWriteResult{Row: updated, CategoryType: category, PreviousFotoAttachmentID: prevFotoAttachmentID}, nil
|
|
}
|
|
|
|
func cloneOperationalShiftTimeInputs(items []BaseOperationalShiftTimeInput) []base.BaseOperationalShiftTime {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]base.BaseOperationalShiftTime, 0, len(items))
|
|
for i := range items {
|
|
dateStart := items[i].DateStart.UTC()
|
|
dateEnd := items[i].DateEnd.UTC()
|
|
out = append(out, base.BaseOperationalShiftTime{
|
|
DateStart: &dateStart,
|
|
DateEnd: &dateEnd,
|
|
StartTimeType: normalizeShiftTimeType(items[i].StartTimeType),
|
|
EndTimeType: normalizeShiftTimeType(items[i].EndTimeType),
|
|
ShiftStart: timePtrToClockString(items[i].ShiftStart),
|
|
ShiftEnd: timePtrToClockString(items[i].ShiftEnd),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func timePtrToClockString(v *time.Time) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
return v.UTC().Format("15:04:05")
|
|
}
|
|
|
|
func normalizeUTCString(raw string) string {
|
|
return strings.TrimSpace(raw)
|
|
}
|
|
|
|
func normalizeShiftTimeType(raw string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(raw)) {
|
|
case "", base.ShiftTimeTypeFixed:
|
|
return base.ShiftTimeTypeFixed
|
|
case base.ShiftTimeTypeBMCT:
|
|
return base.ShiftTimeTypeBMCT
|
|
case base.ShiftTimeTypeECET:
|
|
return base.ShiftTimeTypeECET
|
|
default:
|
|
return strings.ToUpper(strings.TrimSpace(raw))
|
|
}
|
|
}
|
|
|
|
func appendUniqueIDs(values [][]byte) [][]byte {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make([][]byte, 0, len(values))
|
|
seen := map[string]struct{}{}
|
|
for i := range values {
|
|
if len(values[i]) != 16 {
|
|
continue
|
|
}
|
|
key := string(values[i])
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, append([]byte(nil), values[i]...))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func categoryNotFoundDetail(category string) string {
|
|
if category == base.CategoryKeyHEMS {
|
|
return "hems base not found"
|
|
}
|
|
return "base not found"
|
|
}
|