1586 lines
58 KiB
Go
1586 lines
58 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
mysqldriver "github.com/go-sql-driver/mysql"
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
|
|
facilitydomain "wucher/internal/domain/facility"
|
|
flightdata "wucher/internal/domain/flight_data"
|
|
hospitaldomain "wucher/internal/domain/hospital"
|
|
icaodomain "wucher/internal/domain/icao"
|
|
missiondomain "wucher/internal/domain/mission"
|
|
userdomain "wucher/internal/domain/user"
|
|
sharedconst "wucher/internal/shared/const"
|
|
"wucher/internal/shared/pkg/apperrorsx"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
"wucher/internal/transport/http/dto"
|
|
requestdto "wucher/internal/transport/http/dto/request"
|
|
responsedto "wucher/internal/transport/http/dto/response"
|
|
"wucher/internal/transport/http/jsonapi"
|
|
"wucher/internal/transport/http/response"
|
|
"wucher/internal/transport/http/validators"
|
|
)
|
|
|
|
type FlightDataHandler struct {
|
|
svc flightdata.Service
|
|
mission missionService
|
|
hospital hospitalService
|
|
icao icaoService
|
|
user userService
|
|
facility facilityService
|
|
validate *validators.Validator
|
|
}
|
|
|
|
func NewFlightDataHandler(svc flightdata.Service) *FlightDataHandler {
|
|
return &FlightDataHandler{
|
|
svc: svc,
|
|
validate: validators.New(),
|
|
}
|
|
}
|
|
|
|
type hospitalService interface {
|
|
GetByID(context.Context, []byte) (*hospitaldomain.Hospital, error)
|
|
}
|
|
|
|
type missionService interface {
|
|
GetByID(context.Context, []byte) (*missiondomain.Mission, error)
|
|
}
|
|
|
|
type icaoService interface {
|
|
GetByID(context.Context, []byte) (*icaodomain.ICAO, error)
|
|
}
|
|
|
|
type userService interface {
|
|
GetByID(context.Context, []byte) (*userdomain.User, error)
|
|
}
|
|
|
|
type facilityService interface {
|
|
GetByID(context.Context, []byte) (*facilitydomain.Facility, error)
|
|
}
|
|
|
|
func (h *FlightDataHandler) WithHospitalService(svc hospitalService) *FlightDataHandler {
|
|
if h != nil {
|
|
h.hospital = svc
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *FlightDataHandler) WithMissionService(svc missionService) *FlightDataHandler {
|
|
if h != nil {
|
|
h.mission = svc
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *FlightDataHandler) WithICAOService(svc icaoService) *FlightDataHandler {
|
|
if h != nil {
|
|
h.icao = svc
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *FlightDataHandler) WithUserService(svc userService) *FlightDataHandler {
|
|
if h != nil {
|
|
h.user = svc
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *FlightDataHandler) WithFacilityService(svc facilityService) *FlightDataHandler {
|
|
if h != nil {
|
|
h.facility = svc
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *FlightDataHandler) validateFlightDataReferences(ctx context.Context, missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID []byte) error {
|
|
if len(missionID) == 16 && h != nil && h.mission != nil {
|
|
row, err := h.mission.GetByID(ctx, missionID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return flightdata.ErrMissionReference
|
|
}
|
|
return err
|
|
}
|
|
if row == nil {
|
|
return flightdata.ErrMissionReference
|
|
}
|
|
}
|
|
if len(coPilotID) == 16 && h != nil && h.user != nil {
|
|
row, err := h.user.GetByID(ctx, coPilotID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return flightdata.ErrCoPilotReference
|
|
}
|
|
return err
|
|
}
|
|
if row == nil {
|
|
return flightdata.ErrCoPilotReference
|
|
}
|
|
}
|
|
if len(fromICAOID) == 16 && h != nil && h.icao != nil {
|
|
row, err := h.icao.GetByID(ctx, fromICAOID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return flightdata.ErrOriginLocationReference
|
|
}
|
|
return err
|
|
}
|
|
if row == nil {
|
|
return flightdata.ErrOriginLocationReference
|
|
}
|
|
}
|
|
if len(toICAOID) == 16 && h != nil && h.icao != nil {
|
|
row, err := h.icao.GetByID(ctx, toICAOID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return flightdata.ErrDestinationLocationReference
|
|
}
|
|
return err
|
|
}
|
|
if row == nil {
|
|
return flightdata.ErrDestinationLocationReference
|
|
}
|
|
}
|
|
if len(fromHospitalID) == 16 && h != nil && h.hospital != nil {
|
|
row, err := h.hospital.GetByID(ctx, fromHospitalID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return flightdata.ErrOriginLocationReference
|
|
}
|
|
return err
|
|
}
|
|
if row == nil {
|
|
return flightdata.ErrOriginLocationReference
|
|
}
|
|
}
|
|
if len(toHospitalID) == 16 && h != nil && h.hospital != nil {
|
|
row, err := h.hospital.GetByID(ctx, toHospitalID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return flightdata.ErrDestinationLocationReference
|
|
}
|
|
return err
|
|
}
|
|
if row == nil {
|
|
return flightdata.ErrDestinationLocationReference
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *FlightDataHandler) validateSPOFacilityReferences(ctx context.Context, attrs requestdto.FlightDataCreateAttributes) *jsonapi.ErrorObject {
|
|
if h == nil || h.facility == nil {
|
|
return nil
|
|
}
|
|
|
|
check := func(pointer, raw, expectedCategory string) *jsonapi.ErrorObject {
|
|
id, err := uuidv7.ParseString(strings.TrimSpace(raw))
|
|
if err != nil || len(id) != 16 {
|
|
return nil
|
|
}
|
|
row, err := h.facility.GetByID(ctx, id)
|
|
if err != nil || row == nil || !strings.EqualFold(strings.TrimSpace(row.Category), expectedCategory) {
|
|
return &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageFacilityReference,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if attrs.HESLO != nil {
|
|
for i, f := range attrs.HESLO.Flights {
|
|
if errObj := check("/data/attributes/heslo/flights/"+strconv.Itoa(i)+"/heslo_rope_label_id", f.HESLORopeLabelID, sharedconst.FlightDataFacilityCategoryHesloRopeLabel); errObj != nil {
|
|
return errObj
|
|
}
|
|
for j, s := range f.Slings {
|
|
if errObj := check("/data/attributes/heslo/flights/"+strconv.Itoa(i)+"/slings/"+strconv.Itoa(j)+"/sling_id", s.SlingID, sharedconst.FlightDataFacilityCategoryHesloRopeLength); errObj != nil {
|
|
return errObj
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if attrs.Logging != nil {
|
|
for i, s := range attrs.Logging.Slings {
|
|
if errObj := check("/data/attributes/logging/slings/"+strconv.Itoa(i)+"/sling_id", s.SlingID, sharedconst.FlightDataFacilityCategoryHesloHook); errObj != nil {
|
|
return errObj
|
|
}
|
|
}
|
|
}
|
|
|
|
if attrs.HEC != nil {
|
|
for i, f := range attrs.HEC.Flights {
|
|
if errObj := check("/data/attributes/hec/flights/"+strconv.Itoa(i)+"/equipment_id", f.EquipmentID, sharedconst.FlightDataFacilityCategoryHecRopeLabel); errObj != nil {
|
|
return errObj
|
|
}
|
|
}
|
|
for i, s := range attrs.HEC.Slings {
|
|
if errObj := check("/data/attributes/hec/slings/"+strconv.Itoa(i)+"/sling_id", s.SlingID, sharedconst.FlightDataFacilityCategoryHecRopeLength); errObj != nil {
|
|
return errObj
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListFlightData godoc
|
|
// @Summary List flight data
|
|
// @Description List flight data rows. Supports `date` for exact flight take-off date filtering (`YYYY-MM-DD`), plus `mission_id` and `flight_data_id` filters.
|
|
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
|
|
// @Tags Mission - Flight Data
|
|
// @Produce json
|
|
// @Param date query string false "Filter by flight take-off date (YYYY-MM-DD)"
|
|
// @Param mission_id query string false "Filter by mission UUID"
|
|
// @Param flight_data_id query string false "Filter by flight-data UUID"
|
|
// @Param page[number] query int false "Page number"
|
|
// @Param page[size] query int false "Page size"
|
|
// @Param page query int false "Page number (fallback)"
|
|
// @Param size query int false "Page size (fallback)"
|
|
// @Param limit query int false "Page size override"
|
|
// @Success 200 {object} responsedto.FlightDataListResponse
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/flight-data/get-all [get]
|
|
func (h *FlightDataHandler) List(c *fiber.Ctx) error {
|
|
dateFilter := strings.TrimSpace(c.Query("date"))
|
|
if dateFilter != "" {
|
|
if _, err := time.Parse("2006-01-02", dateFilter); err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "date must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/date"},
|
|
}})
|
|
}
|
|
}
|
|
missionQuery := c.Query("mission_id")
|
|
missionID, errObj := parseOptionalUUIDFlightData(&missionQuery, "/query/mission_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
flightDataQuery := c.Query("flight_data_id")
|
|
flightDataID, errObj := parseOptionalUUIDFlightData(&flightDataQuery, "/query/flight_data_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
pageNumber := parseIntDefault(c.Query("page[number]"), 0)
|
|
if pageNumber <= 0 {
|
|
pageNumber = parseIntDefault(c.Query("page"), 1)
|
|
}
|
|
pageSize := parseIntDefault(c.Query("page[size]"), 0)
|
|
if pageSize <= 0 {
|
|
pageSize = parseIntDefault(c.Query("limit"), 0)
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = parseIntDefault(c.Query("size"), 20)
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
if pageNumber < 1 {
|
|
pageNumber = 1
|
|
}
|
|
|
|
offset := (pageNumber - 1) * pageSize
|
|
_ = offset
|
|
rows, total, err := h.svc.List(c.UserContext(), dateFilter, missionID, flightDataID, 0, 0)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
|
|
data := make([]responsedto.FlightDataResource, 0, len(rows))
|
|
for i := range rows {
|
|
resource, buildErr := h.buildFlightDataResource(c, &rows[i])
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
data = append(data, resource)
|
|
}
|
|
|
|
return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
// ListFlightDataDatatable godoc
|
|
// @Summary List flight data (datatable)
|
|
// @Description Datatable-friendly list of flight data rows. Supports `date` for exact flight take-off date filtering (`YYYY-MM-DD`), plus `mission_id` and `flight_data_id` filters.
|
|
// @Description Same payload shape as `get-all`, but with DataTables paging params (`start`, `length`, `draw`).
|
|
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
|
|
// @Tags Mission - Flight Data
|
|
// @Produce json
|
|
// @Param draw query int false "DataTables draw counter"
|
|
// @Param start query int false "Paging start offset"
|
|
// @Param length query int false "Paging length"
|
|
// @Param page[number] query int false "Page number"
|
|
// @Param page[size] query int false "Page size"
|
|
// @Param page query int false "Page number (fallback)"
|
|
// @Param size query int false "Page size (fallback)"
|
|
// @Param limit query int false "Page size override"
|
|
// @Param date query string false "Filter by flight take-off date (YYYY-MM-DD)"
|
|
// @Param mission_id query string false "Filter by mission UUID"
|
|
// @Param flight_data_id query string false "Filter by flight-data UUID"
|
|
// @Success 200 {object} responsedto.FlightDataListResponse
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/flight-data/get-all/dt [get]
|
|
func (h *FlightDataHandler) ListDatatable(c *fiber.Ctx) error {
|
|
dateFilter := strings.TrimSpace(c.Query("date"))
|
|
if dateFilter != "" {
|
|
if _, err := time.Parse("2006-01-02", dateFilter); err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "date must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/date"},
|
|
}})
|
|
}
|
|
}
|
|
missionQuery := c.Query("mission_id")
|
|
missionID, errObj := parseOptionalUUIDFlightData(&missionQuery, "/query/mission_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
flightDataQuery := c.Query("flight_data_id")
|
|
flightDataID, errObj := parseOptionalUUIDFlightData(&flightDataQuery, "/query/flight_data_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
pageNumber := parseIntDefault(c.Query("page[number]"), 0)
|
|
if pageNumber <= 0 {
|
|
pageNumber = parseIntDefault(c.Query("page"), 1)
|
|
}
|
|
pageSize := parseIntDefault(c.Query("page[size]"), 0)
|
|
if pageSize <= 0 {
|
|
pageSize = parseIntDefault(c.Query("limit"), 0)
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = parseIntDefault(c.Query("size"), 20)
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
if pageNumber < 1 {
|
|
pageNumber = 1
|
|
}
|
|
|
|
if start := parseIntDefault(c.Query("start"), -1); start >= 0 && c.Query("length") != "" {
|
|
pageSize = parseIntDefault(c.Query("length"), pageSize)
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
pageNumber = (start / pageSize) + 1
|
|
}
|
|
|
|
offset := (pageNumber - 1) * pageSize
|
|
rows, total, err := h.svc.List(c.UserContext(), dateFilter, missionID, flightDataID, pageSize, offset)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
|
|
data := make([]responsedto.FlightDataResource, 0, len(rows))
|
|
for i := range rows {
|
|
resource, buildErr := h.buildFlightDataResource(c, &rows[i])
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
data = append(data, resource)
|
|
}
|
|
|
|
return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
|
|
"page_number": pageNumber,
|
|
"page_size": pageSize,
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
// CreateFlightData godoc
|
|
// @Summary Create flight data
|
|
// @Description Create a flight data row.
|
|
// @Description Required field: `mission_id`. `co_pilot_id` is optional.
|
|
// @Description Swagger request example shows the complete common payload for an `SPO` mission. `SPO` is the default example shape, and for other mission types (`CAT` and `NCO`) you do not need to send the `hec`, `heslo`, or `logging` blocks. `rotor_brake_cycle` is optional for `SPO`, `CAT`, and `NCO`.
|
|
// @Description SPO facility references must point to facilities with categories: `heslo_rope_label_id -> heslo-rope-label`, `heslo.slings -> heslo-rope-length`, `logging.slings -> heslo-hook`, `hec.flights -> hec-rope-label`, and `hec.slings -> hec-rope-length`.
|
|
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
|
|
// @Description Example request: {"data":{"type":"flight_data_create","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13"}}}
|
|
// @Tags Mission - Flight Data
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.FlightDataCreateRequest true "JSON:API flight data create request. Example is for SPO; CAT/NCO can omit SPO-only blocks and other optional fields." SchemaExample({"data":{"type":"flight_data_create","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","flight_take_off":"2026-04-20T08:00:00Z","flight_landing":"2026-04-20T09:15:00Z","from_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","to_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","hook_releases":0,"ticket_no":"SPO-001","engine":"AIRBUS H145","delivery_note_number":"DN-2026-0001","customer_name":"Patient Transfer","other_information":"SPO mission sample","landing_count":1,"heslo":{"flights":[{"id":"string","rot":0,"heslo_rope_label_id":"string","heslo_rope_label_name":"string"}],"slings":[{"id":"string","heslo_flight_id":"string","sling_id":"string","sling_name":"string"}]},"logging":{"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]},"hec":{"flights":[{"equipment_id":"string","equipment_name":"string","hec_cycle":0,"id":"string","rot":0}],"loads":[{"id":"string","load_category":"string","quantity":0}],"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]}}}})
|
|
// @Success 201 {object} responsedto.FlightDataResponse
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/flight-data/create [post]
|
|
func (h *FlightDataHandler) Create(c *fiber.Ctx) error {
|
|
var req requestdto.FlightDataCreateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
|
|
attrs := req.Data.Attributes
|
|
|
|
missionID, errObj := parseRequiredUUID(attrs.MissionID, "/data/attributes/mission_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
coPilotID, errObj := parseOptionalUUIDFlightData(attrs.CoPilotID, "/data/attributes/co_pilot_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
fromICAOID, errObj := parseOptionalUUIDFlightData(attrs.FromICAOID, "/data/attributes/from_icao_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
fromHospitalID, errObj := parseOptionalUUIDFlightData(attrs.FromHospitalID, "/data/attributes/from_hospital_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
toICAOID, errObj := parseOptionalUUIDFlightData(attrs.ToICAOID, "/data/attributes/to_icao_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
toHospitalID, errObj := parseOptionalUUIDFlightData(attrs.ToHospitalID, "/data/attributes/to_hospital_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if err := h.validateFlightDataReferences(c.UserContext(), missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID); err != nil {
|
|
return writeFlightDataServiceError(c, err, missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID)
|
|
}
|
|
|
|
row := &flightdata.FlightData{
|
|
MissionID: missionID,
|
|
CoPilotID: coPilotID,
|
|
FromICAOID: fromICAOID,
|
|
FromHospitalID: fromHospitalID,
|
|
ToICAOID: toICAOID,
|
|
ToHospitalID: toHospitalID,
|
|
CreatedBy: actorUserID(c),
|
|
UpdatedBy: actorUserID(c),
|
|
TicketNo: stringPtrOrEmpty(attrs.TicketNo),
|
|
Engine: stringPtrOrEmpty(attrs.Engine),
|
|
DeliveryNoteNumber: stringPtrOrEmpty(attrs.DeliveryNoteNumber),
|
|
CustomerName: stringPtrOrEmpty(attrs.CustomerName),
|
|
OtherInformation: stringPtrOrEmpty(attrs.OtherInformation),
|
|
}
|
|
if attrs.FlightTakeOff != nil {
|
|
v, err := parseRFC3339(*attrs.FlightTakeOff, "/data/attributes/flight_take_off")
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
|
|
}
|
|
row.FlightTakeOff = v
|
|
}
|
|
if attrs.FlightLanding != nil {
|
|
v, err := parseRFC3339(*attrs.FlightLanding, "/data/attributes/flight_landing")
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
|
|
}
|
|
row.FlightLanding = v
|
|
}
|
|
if attrs.FlightDurationSecs != nil {
|
|
row.FlightDuration = time.Duration(*attrs.FlightDurationSecs) * time.Second
|
|
}
|
|
if attrs.FlightREDSecs != nil {
|
|
row.FlightRED = time.Duration(*attrs.FlightREDSecs) * time.Second
|
|
}
|
|
if attrs.MaxN1 != nil {
|
|
row.MaxN1 = *attrs.MaxN1
|
|
}
|
|
if attrs.MaxN2 != nil {
|
|
row.MaxN2 = *attrs.MaxN2
|
|
}
|
|
if attrs.PaxCount != nil {
|
|
row.PaxCount = *attrs.PaxCount
|
|
}
|
|
if attrs.LandingCount != nil {
|
|
row.LandingCount = *attrs.LandingCount
|
|
}
|
|
if attrs.RotorBrakeCycle != nil {
|
|
row.RotorBrakeCycle = *attrs.RotorBrakeCycle
|
|
}
|
|
if attrs.HookReleases != nil {
|
|
row.HookReleases = *attrs.HookReleases
|
|
}
|
|
if attrs.FlightPlanDistance != nil {
|
|
row.FlightPlanDistance = *attrs.FlightPlanDistance
|
|
}
|
|
if attrs.FlightPlanTimeSecs != nil {
|
|
row.FlightPlanTime = time.Duration(*attrs.FlightPlanTimeSecs) * time.Second
|
|
}
|
|
if attrs.FlightPlanTrueCourse != nil {
|
|
row.FlightPlanTrueCourse = *attrs.FlightPlanTrueCourse
|
|
}
|
|
if attrs.FuelBeforeFlight != nil {
|
|
row.FuelBeforeFlight = *attrs.FuelBeforeFlight
|
|
}
|
|
if attrs.FuelUpload != nil {
|
|
row.FuelUpload = *attrs.FuelUpload
|
|
}
|
|
if attrs.FuelAfterFlight != nil {
|
|
row.FuelAfterFlight = *attrs.FuelAfterFlight
|
|
}
|
|
if attrs.FuelPlanning != nil {
|
|
row.FuelPlanning = *attrs.FuelPlanning
|
|
}
|
|
if attrs.FlightPositioning != nil {
|
|
row.FlightPositioning = *attrs.FlightPositioning
|
|
}
|
|
|
|
if err := h.svc.Create(c.UserContext(), row); err != nil {
|
|
return writeFlightDataServiceError(c, err, row.MissionID, row.CoPilotID, row.FromICAOID, row.FromHospitalID, row.ToICAOID, row.ToHospitalID)
|
|
}
|
|
|
|
if spoData := parseSPOUpsertData(attrs); spoData != nil {
|
|
if errObj := h.validateSPOFacilityReferences(c.UserContext(), attrs); errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if err := h.svc.UpsertSPODetails(c.UserContext(), row.ID, spoData); err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
}
|
|
|
|
created, err := h.svc.GetByID(c.UserContext(), row.ID)
|
|
if err == nil && created != nil {
|
|
resource, buildErr := h.buildFlightDataResource(c, created)
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
return response.Write(c, fiber.StatusCreated, resource)
|
|
}
|
|
if rowByMission, lookupErr := h.svc.GetByMissionID(c.UserContext(), row.MissionID); lookupErr == nil && rowByMission != nil {
|
|
resource, buildErr := h.buildFlightDataResource(c, rowByMission)
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
return response.Write(c, fiber.StatusCreated, resource)
|
|
}
|
|
resource, buildErr := h.buildFlightDataResource(c, row)
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
return response.Write(c, fiber.StatusCreated, resource)
|
|
}
|
|
|
|
// GetFlightData godoc
|
|
// @Summary Get flight data by ID
|
|
// @Description Get flight data detail by ID. Path `id` can be mission UUID, flight-data UUID, or flight UUID.
|
|
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
|
|
// @Tags Mission - Flight Data
|
|
// @Produce json
|
|
// @Param id path string true "Mission UUID / Flight Data UUID / Flight UUID (UUIDv7)"
|
|
// @Success 200 {object} responsedto.FlightDataResponse
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/flight-data/get/{id} [get]
|
|
func (h *FlightDataHandler) Get(c *fiber.Ctx) error {
|
|
rawID := strings.TrimSpace(c.Params("id"))
|
|
parsedID, err := uuidv7.ParseString(rawID)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageInvalidPathUUID,
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
|
|
}})
|
|
}
|
|
|
|
// Primary flow: mission UUID -> flight_data by mission relation.
|
|
row, err := h.svc.GetByMissionID(c.UserContext(), parsedID)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if row == nil {
|
|
// Backward compatibility for older consumers.
|
|
row, err = h.svc.GetByID(c.UserContext(), parsedID)
|
|
}
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if row == nil {
|
|
row, err = h.svc.GetByFlightID(c.UserContext(), parsedID)
|
|
}
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if row == nil {
|
|
return writeFlightDataErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: flightdata.ErrorMessageNotFound,
|
|
}})
|
|
}
|
|
resource, buildErr := h.buildFlightDataResource(c, row)
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
return response.Write(c, fiber.StatusOK, resource)
|
|
}
|
|
|
|
// UpdateFlightData godoc
|
|
// @Summary Update flight data by ID
|
|
// @Description Update a flight data row by UUID.
|
|
// @Description Required field: `mission_id`. `co_pilot_id` is optional.
|
|
// @Description Swagger request example uses the same SPO-shaped payload as create, but this endpoint updates an existing flight-data row.
|
|
// @Description SPO facility references must point to facilities with categories: `heslo_rope_label_id -> heslo-rope-label`, `heslo.slings -> heslo-rope-length`, `logging.slings -> heslo-hook`, `hec.flights -> hec-rope-label`, and `hec.slings -> hec-rope-length`.
|
|
// @Description For frontend usage, send only the fields that need to change; omitted fields stay unchanged. `SPO` is the default example shape, and for `CAT`/`NCO` you do not need to send the `hec`, `heslo`, or `logging` blocks. `rotor_brake_cycle` is optional for `SPO`, `CAT`, and `NCO`.
|
|
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
|
|
// @Description Example request: {"data":{"type":"flight_data_create","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13"}}}
|
|
// @Tags Mission - Flight Data
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Flight Data UUID (UUIDv7)"
|
|
// @Param request body requestdto.FlightDataUpdateRequest true "JSON:API flight data update request. Send only fields you want to change; omit the rest." SchemaExample({"data":{"type":"flight_data_update","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","flight_take_off":"2026-04-20T08:00:00Z","flight_landing":"2026-04-20T09:15:00Z","from_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","to_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","hook_releases":0,"ticket_no":"SPO-001","engine":"AIRBUS H145","delivery_note_number":"DN-2026-0001","customer_name":"Patient Transfer","other_information":"SPO mission sample","landing_count":1,"heslo":{"flights":[{"id":"string","rot":0,"heslo_rope_label_id":"string","heslo_rope_label_name":"string"}],"slings":[{"id":"string","heslo_flight_id":"string","sling_id":"string","sling_name":"string"}]},"logging":{"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]},"hec":{"flights":[{"equipment_id":"string","equipment_name":"string","hec_cycle":0,"id":"string","rot":0}],"loads":[{"id":"string","load_category":"string","quantity":0}],"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]}}}})
|
|
// @Success 200 {object} responsedto.FlightDataResponse
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/flight-data/update/{id} [patch]
|
|
func (h *FlightDataHandler) Update(c *fiber.Ctx) error {
|
|
rawID := strings.TrimSpace(c.Params("id"))
|
|
id, err := uuidv7.ParseString(rawID)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageInvalidPathUUID,
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
|
|
}})
|
|
}
|
|
|
|
var req requestdto.FlightDataUpdateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
|
|
attrs := req.Data.Attributes
|
|
missionID, errObj := parseRequiredUUID(attrs.MissionID, "/data/attributes/mission_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
coPilotID, errObj := parseOptionalUUIDFlightData(attrs.CoPilotID, "/data/attributes/co_pilot_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
fromICAOID, errObj := parseOptionalUUIDFlightData(attrs.FromICAOID, "/data/attributes/from_icao_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
fromHospitalID, errObj := parseOptionalUUIDFlightData(attrs.FromHospitalID, "/data/attributes/from_hospital_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
toICAOID, errObj := parseOptionalUUIDFlightData(attrs.ToICAOID, "/data/attributes/to_icao_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
toHospitalID, errObj := parseOptionalUUIDFlightData(attrs.ToHospitalID, "/data/attributes/to_hospital_id")
|
|
if errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if err := h.validateFlightDataReferences(c.UserContext(), missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID); err != nil {
|
|
return writeFlightDataServiceError(c, err, missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID)
|
|
}
|
|
|
|
row, err := h.svc.GetByID(c.UserContext(), id)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if row == nil {
|
|
return writeFlightDataErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: flightdata.ErrorMessageNotFound,
|
|
}})
|
|
}
|
|
|
|
row.MissionID = missionID
|
|
if len(coPilotID) > 0 {
|
|
row.CoPilotID = coPilotID
|
|
}
|
|
row.FromICAOID = fromICAOID
|
|
row.FromHospitalID = fromHospitalID
|
|
row.ToICAOID = toICAOID
|
|
row.ToHospitalID = toHospitalID
|
|
row.UpdatedBy = actorUserID(c)
|
|
if attrs.TicketNo != nil {
|
|
row.TicketNo = stringPtrOrEmpty(attrs.TicketNo)
|
|
}
|
|
if attrs.Engine != nil {
|
|
row.Engine = stringPtrOrEmpty(attrs.Engine)
|
|
}
|
|
if attrs.DeliveryNoteNumber != nil {
|
|
row.DeliveryNoteNumber = stringPtrOrEmpty(attrs.DeliveryNoteNumber)
|
|
}
|
|
if attrs.CustomerName != nil {
|
|
row.CustomerName = stringPtrOrEmpty(attrs.CustomerName)
|
|
}
|
|
if attrs.OtherInformation != nil {
|
|
row.OtherInformation = stringPtrOrEmpty(attrs.OtherInformation)
|
|
}
|
|
if attrs.FlightTakeOff != nil {
|
|
v, err := parseRFC3339(*attrs.FlightTakeOff, "/data/attributes/flight_take_off")
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
|
|
}
|
|
row.FlightTakeOff = v
|
|
}
|
|
if attrs.FlightLanding != nil {
|
|
v, err := parseRFC3339(*attrs.FlightLanding, "/data/attributes/flight_landing")
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
|
|
}
|
|
row.FlightLanding = v
|
|
}
|
|
if attrs.FlightDurationSecs != nil {
|
|
row.FlightDuration = time.Duration(*attrs.FlightDurationSecs) * time.Second
|
|
}
|
|
if attrs.FlightREDSecs != nil {
|
|
row.FlightRED = time.Duration(*attrs.FlightREDSecs) * time.Second
|
|
}
|
|
if attrs.MaxN1 != nil {
|
|
row.MaxN1 = *attrs.MaxN1
|
|
}
|
|
if attrs.MaxN2 != nil {
|
|
row.MaxN2 = *attrs.MaxN2
|
|
}
|
|
if attrs.PaxCount != nil {
|
|
row.PaxCount = *attrs.PaxCount
|
|
}
|
|
if attrs.LandingCount != nil {
|
|
row.LandingCount = *attrs.LandingCount
|
|
}
|
|
if attrs.RotorBrakeCycle != nil {
|
|
row.RotorBrakeCycle = *attrs.RotorBrakeCycle
|
|
}
|
|
if attrs.HookReleases != nil {
|
|
row.HookReleases = *attrs.HookReleases
|
|
}
|
|
if attrs.FlightPlanDistance != nil {
|
|
row.FlightPlanDistance = *attrs.FlightPlanDistance
|
|
}
|
|
if attrs.FlightPlanTimeSecs != nil {
|
|
row.FlightPlanTime = time.Duration(*attrs.FlightPlanTimeSecs) * time.Second
|
|
}
|
|
if attrs.FlightPlanTrueCourse != nil {
|
|
row.FlightPlanTrueCourse = *attrs.FlightPlanTrueCourse
|
|
}
|
|
if attrs.FuelBeforeFlight != nil {
|
|
row.FuelBeforeFlight = *attrs.FuelBeforeFlight
|
|
}
|
|
if attrs.FuelUpload != nil {
|
|
row.FuelUpload = *attrs.FuelUpload
|
|
}
|
|
if attrs.FuelAfterFlight != nil {
|
|
row.FuelAfterFlight = *attrs.FuelAfterFlight
|
|
}
|
|
if attrs.FuelPlanning != nil {
|
|
row.FuelPlanning = *attrs.FuelPlanning
|
|
}
|
|
if attrs.FlightPositioning != nil {
|
|
row.FlightPositioning = *attrs.FlightPositioning
|
|
}
|
|
|
|
if err := h.svc.Update(c.UserContext(), row); err != nil {
|
|
return writeFlightDataServiceError(c, err, row.MissionID, row.CoPilotID, row.FromICAOID, row.FromHospitalID, row.ToICAOID, row.ToHospitalID)
|
|
}
|
|
|
|
if spoData := parseSPOUpsertData(attrs); spoData != nil {
|
|
if errObj := h.validateSPOFacilityReferences(c.UserContext(), attrs); errObj != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if err := h.svc.UpsertSPODetails(c.UserContext(), row.ID, spoData); err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
}
|
|
|
|
updated, err := h.svc.GetByID(c.UserContext(), row.ID)
|
|
if err == nil && updated != nil {
|
|
resource, buildErr := h.buildFlightDataResource(c, updated)
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
return response.Write(c, fiber.StatusOK, resource)
|
|
}
|
|
resource, buildErr := h.buildFlightDataResource(c, row)
|
|
if buildErr != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: buildErr.Error(),
|
|
}})
|
|
}
|
|
return response.Write(c, fiber.StatusOK, resource)
|
|
}
|
|
|
|
// DeleteFlightData godoc
|
|
// @Summary Delete flight data by ID
|
|
// @Description Soft delete flight data by UUID.
|
|
// @Tags Mission - Flight Data
|
|
// @Produce json
|
|
// @Param id path string true "Flight Data UUID (UUIDv7)"
|
|
// @Success 200 {object} dto.GenericDeleteResponse
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/flight-data/delete/{id} [delete]
|
|
func (h *FlightDataHandler) Delete(c *fiber.Ctx) error {
|
|
rawID := strings.TrimSpace(c.Params("id"))
|
|
id, err := uuidv7.ParseString(rawID)
|
|
if err != nil {
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageInvalidPathUUID,
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
|
|
}})
|
|
}
|
|
if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return writeFlightDataErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "flight data not found",
|
|
}})
|
|
}
|
|
return writeFlightDataServiceError(c, err)
|
|
}
|
|
return response.Write(c, fiber.StatusOK, dto.NewGenericDeleteResponse("flight_data_delete"))
|
|
}
|
|
|
|
func flightDataResource(row *flightdata.FlightData) responsedto.FlightDataResource {
|
|
resource := responsedto.FlightDataResource{
|
|
Type: sharedconst.FlightDataResourceType,
|
|
ID: idString(row.ID),
|
|
Attributes: buildBaseFlightDataAttributes(row),
|
|
}
|
|
|
|
return resource
|
|
}
|
|
|
|
func (h *FlightDataHandler) buildFlightDataResource(c *fiber.Ctx, row *flightdata.FlightData) (responsedto.FlightDataResource, error) {
|
|
return buildFlightDataResourceWithService(c.UserContext(), h.svc, row)
|
|
}
|
|
|
|
func (h *FlightDataHandler) buildSPODetails(c *fiber.Ctx, row *flightdata.FlightData) (
|
|
*responsedto.FlightDataSPOHESLODetails,
|
|
*responsedto.FlightDataSPOLoggingDetails,
|
|
*responsedto.FlightDataSPOHECDetails,
|
|
error,
|
|
) {
|
|
return buildFlightDataSPODetails(c.UserContext(), h.svc, row)
|
|
}
|
|
|
|
func missionRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataMissionRef {
|
|
if row == nil {
|
|
return nil
|
|
}
|
|
if row.Mission == nil && len(row.MissionID) == 0 {
|
|
return nil
|
|
}
|
|
out := &responsedto.FlightDataMissionRef{
|
|
ID: idString(row.MissionID),
|
|
}
|
|
if row.Mission != nil {
|
|
out.ID = idString(row.Mission.ID)
|
|
out.Type = strings.TrimSpace(row.Mission.Type)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func coPilotRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataCoPilotRef {
|
|
if row == nil {
|
|
return nil
|
|
}
|
|
if row.CoPilot == nil && len(row.CoPilotID) == 0 {
|
|
return nil
|
|
}
|
|
out := &responsedto.FlightDataCoPilotRef{
|
|
ID: idString(row.CoPilotID),
|
|
}
|
|
if row.CoPilot != nil {
|
|
out.ID = idString(row.CoPilot.ID)
|
|
out.FirstName = strings.TrimSpace(row.CoPilot.FirstName)
|
|
out.LastName = strings.TrimSpace(row.CoPilot.LastName)
|
|
out.Email = strings.TrimSpace(row.CoPilot.Email)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fromLocationRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataLocationRef {
|
|
if row == nil {
|
|
return nil
|
|
}
|
|
if row.FromICAO != nil {
|
|
code := strings.TrimSpace(row.FromICAO.ICAOCode)
|
|
return &responsedto.FlightDataLocationRef{
|
|
Type: sharedconst.FlightDataLocationTypeICAO,
|
|
ID: idString(row.FromICAO.ID),
|
|
Code: code,
|
|
Label: code,
|
|
}
|
|
}
|
|
if row.FromHospital != nil {
|
|
name := strings.TrimSpace(row.FromHospital.Name)
|
|
return &responsedto.FlightDataLocationRef{
|
|
Type: sharedconst.FlightDataLocationTypeHospital,
|
|
ID: idString(row.FromHospital.ID),
|
|
Name: name,
|
|
Label: name,
|
|
}
|
|
}
|
|
if len(row.FromICAOID) > 0 {
|
|
return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeICAO, ID: idString(row.FromICAOID)}
|
|
}
|
|
if len(row.FromHospitalID) > 0 {
|
|
return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeHospital, ID: idString(row.FromHospitalID)}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func toLocationRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataLocationRef {
|
|
if row == nil {
|
|
return nil
|
|
}
|
|
if row.ToICAO != nil {
|
|
code := strings.TrimSpace(row.ToICAO.ICAOCode)
|
|
return &responsedto.FlightDataLocationRef{
|
|
Type: sharedconst.FlightDataLocationTypeICAO,
|
|
ID: idString(row.ToICAO.ID),
|
|
Code: code,
|
|
Label: code,
|
|
}
|
|
}
|
|
if row.ToHospital != nil {
|
|
name := strings.TrimSpace(row.ToHospital.Name)
|
|
return &responsedto.FlightDataLocationRef{
|
|
Type: sharedconst.FlightDataLocationTypeHospital,
|
|
ID: idString(row.ToHospital.ID),
|
|
Name: name,
|
|
Label: name,
|
|
}
|
|
}
|
|
if len(row.ToICAOID) > 0 {
|
|
return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeICAO, ID: idString(row.ToICAOID)}
|
|
}
|
|
if len(row.ToHospitalID) > 0 {
|
|
return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeHospital, ID: idString(row.ToHospitalID)}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseRequiredUUID(raw, pointer string) ([]byte, *jsonapi.ErrorObject) {
|
|
trimmed := strings.TrimSpace(raw)
|
|
if trimmed == "" {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageFieldRequired,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
v, err := uuidv7.ParseString(trimmed)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageInvalidUUID,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func parseOptionalUUIDFlightData(raw *string, pointer string) ([]byte, *jsonapi.ErrorObject) {
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
trimmed := strings.TrimSpace(*raw)
|
|
if trimmed == "" {
|
|
return nil, nil
|
|
}
|
|
v, err := uuidv7.ParseString(trimmed)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageInvalidUUID,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func parseRFC3339(raw, pointer string) (time.Time, *jsonapi.ErrorObject) {
|
|
v, err := time.Parse(time.RFC3339, strings.TrimSpace(raw))
|
|
if err != nil {
|
|
return time.Time{}, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageInvalidRFC3339,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return v.UTC(), nil
|
|
}
|
|
|
|
func writeFlightDataServiceError(c *fiber.Ctx, err error, refs ...[]byte) error {
|
|
var (
|
|
fromICAOID []byte
|
|
fromHospitalID []byte
|
|
toICAOID []byte
|
|
toHospitalID []byte
|
|
)
|
|
if len(refs) > 2 {
|
|
fromICAOID = refs[2]
|
|
}
|
|
if len(refs) > 3 {
|
|
fromHospitalID = refs[3]
|
|
}
|
|
if len(refs) > 4 {
|
|
toICAOID = refs[4]
|
|
}
|
|
if len(refs) > 5 {
|
|
toHospitalID = refs[5]
|
|
}
|
|
|
|
switch {
|
|
case errors.Is(err, flightdata.ErrMissionReference):
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageMissionReference,
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/mission_id"},
|
|
}})
|
|
case errors.Is(err, flightdata.ErrOriginLocationReference):
|
|
pointer := "/data/attributes/from_icao_id"
|
|
if len(fromICAOID) == 0 && len(fromHospitalID) > 0 {
|
|
pointer = "/data/attributes/from_hospital_id"
|
|
}
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageOriginLocationReference,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}})
|
|
case errors.Is(err, flightdata.ErrDestinationLocationReference):
|
|
pointer := "/data/attributes/to_icao_id"
|
|
if len(toICAOID) == 0 && len(toHospitalID) > 0 {
|
|
pointer = "/data/attributes/to_hospital_id"
|
|
}
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageDestinationLocationReference,
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}})
|
|
case errors.Is(err, flightdata.ErrCoPilotReference):
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: flightdata.ErrorMessageCoPilotReference,
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/co_pilot_id"},
|
|
}})
|
|
case errors.Is(err, flightdata.ErrCreateBlocked):
|
|
return writeFlightDataErrors(c, fiber.StatusConflict, []jsonapi.ErrorObject{{
|
|
Status: "409",
|
|
Title: "Conflict",
|
|
Detail: flightdata.ErrorMessageCreateBlocked,
|
|
}})
|
|
case errors.Is(err, flightdata.ErrLocked):
|
|
return writeFlightDataErrors(c, fiber.StatusLocked, []jsonapi.ErrorObject{{
|
|
Status: "423",
|
|
Title: "Locked",
|
|
Detail: flightdata.ErrorMessageLocked,
|
|
}})
|
|
case errors.Is(err, flightdata.ErrLockedAfterFlightInspection):
|
|
return writeFlightDataErrors(c, fiber.StatusLocked, []jsonapi.ErrorObject{{
|
|
Status: "423",
|
|
Title: "Locked",
|
|
Detail: flightdata.ErrorMessageLockedAfterFlightInspection,
|
|
}})
|
|
case errors.Is(err, flightdata.ErrRequired):
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: flightdata.ErrorMessageRequired,
|
|
}})
|
|
case flightDataFKError(err) != nil:
|
|
obj := flightDataFKError(err)
|
|
return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*obj})
|
|
default:
|
|
return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
}
|
|
|
|
func flightDataFKError(err error) *jsonapi.ErrorObject {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var mysqlErr *mysqldriver.MySQLError
|
|
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1452 {
|
|
return nil
|
|
}
|
|
|
|
if _, errs, ok := mapOperationRelationError(err, map[string]relationFieldError{
|
|
"(`mission_id`)": {Pointer: "/data/attributes/mission_id", Detail: flightdata.ErrorMessageMissionReference},
|
|
"(`from_icao_id`)": {Pointer: "/data/attributes/from_icao_id", Detail: flightdata.ErrorMessageOriginLocationReference},
|
|
"(`from_hospital_id`)": {Pointer: "/data/attributes/from_hospital_id", Detail: flightdata.ErrorMessageOriginLocationReference},
|
|
"(`to_icao_id`)": {Pointer: "/data/attributes/to_icao_id", Detail: flightdata.ErrorMessageDestinationLocationReference},
|
|
"(`to_hospital_id`)": {Pointer: "/data/attributes/to_hospital_id", Detail: flightdata.ErrorMessageDestinationLocationReference},
|
|
"(`pilot_id`)": {Pointer: "/data/attributes/co_pilot_id", Detail: flightdata.ErrorMessageCoPilotReference},
|
|
"(`co_pilot_id`)": {Pointer: "/data/attributes/co_pilot_id", Detail: flightdata.ErrorMessageCoPilotReference},
|
|
}); ok && len(errs) > 0 {
|
|
return &errs[0]
|
|
}
|
|
|
|
return &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "referenced relation data was not found",
|
|
}
|
|
}
|
|
|
|
func writeFlightDataErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error {
|
|
enriched := make([]jsonapi.ErrorObject, 0, len(errs))
|
|
statusStr := strconv.Itoa(status)
|
|
for i := range errs {
|
|
e := errs[i]
|
|
e.Status = statusStr
|
|
def := inferFlightDataDef(status, e.Title, e.Detail, e.Source)
|
|
if strings.TrimSpace(e.Code) == "" {
|
|
e.Code = def.Code
|
|
}
|
|
if strings.TrimSpace(e.ErrorCode) == "" {
|
|
e.ErrorCode = def.ErrorCode
|
|
}
|
|
if strings.TrimSpace(e.Title) == "" {
|
|
e.Title = def.Title
|
|
}
|
|
e.Detail = def.Message
|
|
enriched = append(enriched, e)
|
|
}
|
|
return response.WriteErrors(c, status, enriched)
|
|
}
|
|
|
|
func inferFlightDataDef(status int, title, detail string, source *jsonapi.ErrorSource) apperrorsx.Def {
|
|
lowerTitle := strings.ToLower(strings.TrimSpace(title))
|
|
lowerDetail := strings.ToLower(strings.TrimSpace(detail))
|
|
pointer := ""
|
|
if source != nil {
|
|
pointer = strings.ToLower(strings.TrimSpace(source.Pointer))
|
|
}
|
|
|
|
switch status {
|
|
case fiber.StatusNotFound:
|
|
return apperrorsx.ErrFlightDataNotFound
|
|
case fiber.StatusLocked:
|
|
switch {
|
|
case strings.Contains(lowerDetail, "after flight inspection already exists"):
|
|
return apperrorsx.ErrFlightDataLockedAfterFlightInspection
|
|
case strings.Contains(lowerDetail, "fm report is completed"),
|
|
strings.Contains(lowerDetail, "flight data is locked because fm report is completed"):
|
|
return apperrorsx.ErrFlightDataLocked
|
|
default:
|
|
return apperrorsx.ErrFlightDataLocked
|
|
}
|
|
case fiber.StatusUnprocessableEntity:
|
|
switch {
|
|
case strings.Contains(lowerDetail, "invalid path uuid"), pointer == "/path/uuid":
|
|
return apperrorsx.ErrFlightDataInvalidPathUUID
|
|
case strings.Contains(lowerDetail, "field is required"), pointer == "/data/attributes/mission_id":
|
|
return apperrorsx.ErrFlightDataMissionIDRequired
|
|
case strings.Contains(lowerDetail, "mission reference"):
|
|
return apperrorsx.ErrFlightDataMissionReference
|
|
case strings.Contains(lowerDetail, "field is required"), pointer == "/data/attributes/co_pilot_id", pointer == "/data/attributes/pilot_id":
|
|
return apperrorsx.ErrFlightDataPilotIDRequired
|
|
case strings.Contains(lowerDetail, "pilot must reference an existing user"):
|
|
return apperrorsx.ErrFlightDataPilotIDRequired
|
|
case strings.Contains(lowerDetail, "invalid uuid"):
|
|
return apperrorsx.ErrFlightDataInvalidUUID
|
|
case strings.Contains(lowerDetail, "invalid rfc3339"):
|
|
return apperrorsx.ErrFlightDataInvalidRFC3339
|
|
case strings.Contains(lowerDetail, "origin location reference"):
|
|
return apperrorsx.ErrFlightDataOriginLocationReference
|
|
case strings.Contains(lowerDetail, "destination location reference"):
|
|
return apperrorsx.ErrFlightDataDestinationLocationReference
|
|
case strings.Contains(lowerDetail, "pilot reference"):
|
|
return apperrorsx.ErrFlightDataPilotReference
|
|
case strings.Contains(lowerDetail, "facility reference"):
|
|
return apperrorsx.ErrFlightDataFacilityReference
|
|
case strings.Contains(lowerDetail, "cannot be created"), strings.Contains(lowerDetail, "already exists"):
|
|
return apperrorsx.ErrFlightDataCreateBlocked
|
|
default:
|
|
return apperrorsx.ErrFlightDataInvalidPayload
|
|
}
|
|
case fiber.StatusBadRequest:
|
|
switch {
|
|
case strings.Contains(lowerTitle, "invalid json"):
|
|
return apperrorsx.ErrFlightDataInvalidJSON
|
|
case strings.Contains(lowerTitle, "create failed"):
|
|
if strings.Contains(lowerDetail, "required data is missing") {
|
|
return apperrorsx.ErrFlightDataRequired
|
|
}
|
|
return apperrorsx.ErrFlightDataCreateFailed
|
|
case strings.Contains(lowerTitle, "get failed"):
|
|
return apperrorsx.ErrFlightDataGetFailed
|
|
default:
|
|
return apperrorsx.ErrInvalidRequest
|
|
}
|
|
default:
|
|
return apperrorsx.ErrInternal
|
|
}
|
|
}
|
|
|
|
func inferFlightDataErrorCode(status int, title, detail string, source *jsonapi.ErrorSource) (string, string) {
|
|
def := inferFlightDataDef(status, title, detail, source)
|
|
return def.Code, def.ErrorCode
|
|
}
|
|
|
|
func parseSPOUpsertData(attrs requestdto.FlightDataCreateAttributes) *flightdata.SPOUpsertData {
|
|
if attrs.HESLO == nil && attrs.Logging == nil && attrs.HEC == nil {
|
|
return nil
|
|
}
|
|
data := &flightdata.SPOUpsertData{}
|
|
|
|
if attrs.HESLO != nil {
|
|
heslo := &flightdata.SPOHESLOUpsert{
|
|
Flights: make([]flightdata.SPOHESLOFlightUpsert, 0, len(attrs.HESLO.Flights)),
|
|
}
|
|
for _, f := range attrs.HESLO.Flights {
|
|
fu := flightdata.SPOHESLOFlightUpsert{ROT: f.ROT}
|
|
if id, err := uuidv7.ParseString(f.HESLORopeLabelID); err == nil {
|
|
fu.FuelTruckID = id
|
|
}
|
|
for _, s := range f.Slings {
|
|
if id, err := uuidv7.ParseString(s.SlingID); err == nil {
|
|
fu.Slings = append(fu.Slings, flightdata.SPOHESLOSlingUpsert{SlingID: id})
|
|
}
|
|
}
|
|
heslo.Flights = append(heslo.Flights, fu)
|
|
}
|
|
data.HESLO = heslo
|
|
}
|
|
|
|
if attrs.Logging != nil {
|
|
logging := &flightdata.SPOLoggingUpsert{
|
|
Slings: make([]flightdata.SPOLoggingSlingUpsert, 0, len(attrs.Logging.Slings)),
|
|
}
|
|
for _, s := range attrs.Logging.Slings {
|
|
if id, err := uuidv7.ParseString(s.SlingID); err == nil {
|
|
logging.Slings = append(logging.Slings, flightdata.SPOLoggingSlingUpsert{SlingID: id})
|
|
}
|
|
}
|
|
data.Logging = logging
|
|
}
|
|
|
|
if attrs.HEC != nil {
|
|
hec := &flightdata.SPOHECUpsert{
|
|
Flights: make([]flightdata.SPOHECFlightUpsert, 0, len(attrs.HEC.Flights)),
|
|
Slings: make([]flightdata.SPOHECSlingUpsert, 0, len(attrs.HEC.Slings)),
|
|
Loads: make([]flightdata.SPOHECLoadUpsert, 0, len(attrs.HEC.Loads)),
|
|
}
|
|
for _, f := range attrs.HEC.Flights {
|
|
fu := flightdata.SPOHECFlightUpsert{ROT: f.ROT, HECCycle: f.HECCycle}
|
|
if id, err := uuidv7.ParseString(f.EquipmentID); err == nil {
|
|
fu.EquipmentID = id
|
|
}
|
|
hec.Flights = append(hec.Flights, fu)
|
|
}
|
|
for _, s := range attrs.HEC.Slings {
|
|
if id, err := uuidv7.ParseString(s.SlingID); err == nil {
|
|
hec.Slings = append(hec.Slings, flightdata.SPOHECSlingUpsert{SlingID: id})
|
|
}
|
|
}
|
|
for _, l := range attrs.HEC.Loads {
|
|
hec.Loads = append(hec.Loads, flightdata.SPOHECLoadUpsert{
|
|
LoadCategory: l.LoadCategory,
|
|
Quantity: l.Quantity,
|
|
})
|
|
}
|
|
data.HEC = hec
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// --- Response building (flight data → JSON:API resource) ---
|
|
|
|
func buildFlightDataResourceWithService(ctx context.Context, svc flightdata.Service, row *flightdata.FlightData) (responsedto.FlightDataResource, error) {
|
|
resource := responsedto.FlightDataResource{
|
|
Type: sharedconst.FlightDataResourceType,
|
|
ID: idString(row.ID),
|
|
Attributes: buildBaseFlightDataAttributes(row),
|
|
}
|
|
|
|
heslo, logging, hec, err := buildFlightDataSPODetails(ctx, svc, row)
|
|
if err != nil {
|
|
return responsedto.FlightDataResource{}, err
|
|
}
|
|
resource.Attributes.HESLO = heslo
|
|
resource.Attributes.Logging = logging
|
|
resource.Attributes.HEC = hec
|
|
return resource, nil
|
|
}
|
|
|
|
func buildBaseFlightDataAttributes(row *flightdata.FlightData) responsedto.FlightDataAttributes {
|
|
return responsedto.FlightDataAttributes{
|
|
Status: func() string {
|
|
if status := strings.TrimSpace(row.Status); status != "" {
|
|
return status
|
|
}
|
|
return flightdata.StatusInProgress
|
|
}(),
|
|
Mission: missionRefFlightData(row),
|
|
RosterDetails: rosterDetailsFromFlightData(row),
|
|
CoPilot: coPilotRefFlightData(row),
|
|
From: fromLocationRefFlightData(row),
|
|
To: toLocationRefFlightData(row),
|
|
FlightTakeOff: formatTimeOrEmpty(row.FlightTakeOff),
|
|
FlightLanding: formatTimeOrEmpty(row.FlightLanding),
|
|
FlightDuration: int64(row.FlightDuration.Seconds()),
|
|
FlightRED: int64(row.FlightRED.Seconds()),
|
|
MaxN1: row.MaxN1,
|
|
MaxN2: row.MaxN2,
|
|
PaxCount: row.PaxCount,
|
|
LandingCount: row.LandingCount,
|
|
RotorBrakeCycle: row.RotorBrakeCycle,
|
|
HookReleases: row.HookReleases,
|
|
TicketNo: row.TicketNo,
|
|
Engine: row.Engine,
|
|
DeliveryNoteNumber: row.DeliveryNoteNumber,
|
|
CustomerName: row.CustomerName,
|
|
FlightPlanDistance: row.FlightPlanDistance,
|
|
FlightPlanTimeSeconds: int64(row.FlightPlanTime.Seconds()),
|
|
FlightPlanTrueCourse: row.FlightPlanTrueCourse,
|
|
FuelBeforeFlight: row.FuelBeforeFlight,
|
|
FuelUpload: row.FuelUpload,
|
|
FuelAfterFlight: row.FuelAfterFlight,
|
|
FuelPlanning: row.FuelPlanning,
|
|
FlightPositioning: row.FlightPositioning,
|
|
OtherInformation: row.OtherInformation,
|
|
CreatedAt: formatTimeOrEmpty(row.CreatedAt),
|
|
CreatedBy: uuidStringOrEmpty(row.CreatedBy),
|
|
UpdatedAt: formatTimeOrEmpty(row.UpdatedAt),
|
|
UpdatedBy: uuidStringOrEmpty(row.UpdatedBy),
|
|
}
|
|
}
|
|
|
|
func rosterDetailsFromFlightData(row *flightdata.FlightData) *responsedto.TakeoverDutyRosterDetailsResponse {
|
|
if row == nil || row.Mission == nil || row.Mission.Flight == nil || row.Mission.Flight.Takeover == nil {
|
|
return nil
|
|
}
|
|
takeoverRow := row.Mission.Flight.Takeover
|
|
if len(takeoverRow.RosterCrews) == 0 && len(takeoverRow.OtherPeople) == 0 {
|
|
return nil
|
|
}
|
|
resource := responsedto.DutyRosterResource{
|
|
Attributes: responsedto.DutyRosterAttributes{
|
|
Detail: responsedto.DutyRosterDetailsResponse{
|
|
Pilot: []responsedto.DutyRosterPilotCrew{},
|
|
Doctor: []responsedto.DutyRosterCrew{},
|
|
Rescuer: []responsedto.DutyRosterCrew{},
|
|
Helper: []responsedto.DutyRosterCrew{},
|
|
OtherPerson: []responsedto.DutyRosterCrew{},
|
|
},
|
|
},
|
|
}
|
|
resource = buildDutyRosterResourceFromTakeoverRows(resource, takeoverRow.RosterCrews, row.Mission.Flight)
|
|
resource = buildDutyRosterResourceFromOtherPeople(resource, takeoverRow.OtherPeople)
|
|
detail := takeoverDutyRosterDetailsFromRosterDetails(resource.Attributes.Detail)
|
|
return &detail
|
|
}
|
|
|
|
func formatTimeOrEmpty(value time.Time) string {
|
|
if value.IsZero() {
|
|
return ""
|
|
}
|
|
return value.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
func buildFlightDataSPODetails(
|
|
ctx context.Context,
|
|
svc flightdata.Service,
|
|
row *flightdata.FlightData,
|
|
) (
|
|
*responsedto.FlightDataSPOHESLODetails,
|
|
*responsedto.FlightDataSPOLoggingDetails,
|
|
*responsedto.FlightDataSPOHECDetails,
|
|
error,
|
|
) {
|
|
if svc == nil || row == nil || row.Mission == nil || strings.ToUpper(strings.TrimSpace(row.Mission.Type)) != "SPO" {
|
|
return nil, nil, nil, nil
|
|
}
|
|
if len(row.ID) != 16 {
|
|
return nil, nil, nil, nil
|
|
}
|
|
|
|
details, err := svc.GetSPODetailsByFlightDataID(ctx, row.ID)
|
|
if err != nil || details == nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
heslo := &responsedto.FlightDataSPOHESLODetails{
|
|
Flights: make([]responsedto.FlightDataSPOHESLOFlight, 0, len(details.HESLO.Flights)),
|
|
}
|
|
logging := &responsedto.FlightDataSPOLoggingDetails{
|
|
Slings: make([]responsedto.FlightDataSPOLoggingSling, 0, len(details.Logging.Slings)),
|
|
}
|
|
hec := &responsedto.FlightDataSPOHECDetails{
|
|
Flights: make([]responsedto.FlightDataSPOHECFlight, 0, len(details.HEC.Flights)),
|
|
Slings: make([]responsedto.FlightDataSPOHECSling, 0, len(details.HEC.Slings)),
|
|
Loads: make([]responsedto.FlightDataSPOHECLoad, 0, len(details.HEC.Loads)),
|
|
}
|
|
|
|
slingsByFlightID := make(map[string][]responsedto.FlightDataSPOHESLOSling, len(details.HESLO.Slings))
|
|
for i := range details.HESLO.Slings {
|
|
flightID := idString(details.HESLO.Slings[i].HESLOFlightID)
|
|
slingsByFlightID[flightID] = append(slingsByFlightID[flightID], responsedto.FlightDataSPOHESLOSling{
|
|
ID: idString(details.HESLO.Slings[i].ID),
|
|
SlingID: idString(details.HESLO.Slings[i].SlingID),
|
|
SlingName: strings.TrimSpace(details.HESLO.Slings[i].SlingName),
|
|
})
|
|
}
|
|
|
|
for i := range details.HESLO.Flights {
|
|
flightID := idString(details.HESLO.Flights[i].ID)
|
|
heslo.Flights = append(heslo.Flights, responsedto.FlightDataSPOHESLOFlight{
|
|
ID: flightID,
|
|
ROT: details.HESLO.Flights[i].ROT,
|
|
HESLORopeLabelID: idString(details.HESLO.Flights[i].FuelTruckID),
|
|
HESLORopeLabelName: strings.TrimSpace(details.HESLO.Flights[i].FuelTruckName),
|
|
Slings: slingsByFlightID[flightID],
|
|
})
|
|
}
|
|
for i := range details.Logging.Slings {
|
|
logging.Slings = append(logging.Slings, responsedto.FlightDataSPOLoggingSling{
|
|
ID: idString(details.Logging.Slings[i].ID),
|
|
SlingID: idString(details.Logging.Slings[i].SlingID),
|
|
SlingName: strings.TrimSpace(details.Logging.Slings[i].SlingName),
|
|
})
|
|
}
|
|
for i := range details.HEC.Flights {
|
|
hec.Flights = append(hec.Flights, responsedto.FlightDataSPOHECFlight{
|
|
ID: idString(details.HEC.Flights[i].ID),
|
|
ROT: details.HEC.Flights[i].ROT,
|
|
HECCycle: details.HEC.Flights[i].HECCycle,
|
|
EquipmentID: idString(details.HEC.Flights[i].EquipmentID),
|
|
EquipmentName: strings.TrimSpace(details.HEC.Flights[i].EquipmentName),
|
|
})
|
|
}
|
|
for i := range details.HEC.Slings {
|
|
hec.Slings = append(hec.Slings, responsedto.FlightDataSPOHECSling{
|
|
ID: idString(details.HEC.Slings[i].ID),
|
|
SlingID: idString(details.HEC.Slings[i].SlingID),
|
|
SlingName: strings.TrimSpace(details.HEC.Slings[i].SlingName),
|
|
})
|
|
}
|
|
for i := range details.HEC.Loads {
|
|
hec.Loads = append(hec.Loads, responsedto.FlightDataSPOHECLoad{
|
|
ID: idString(details.HEC.Loads[i].ID),
|
|
LoadCategory: strings.TrimSpace(details.HEC.Loads[i].LoadCategory),
|
|
Quantity: details.HEC.Loads[i].Quantity,
|
|
})
|
|
}
|
|
|
|
return heslo, logging, hec, nil
|
|
}
|