3136 lines
103 KiB
Go
3136 lines
103 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
afterflightinspection "wucher/internal/domain/after_flight_inspection"
|
|
beforeflightinspection "wucher/internal/domain/before_flight_inspection"
|
|
"wucher/internal/domain/duty_roster"
|
|
"wucher/internal/domain/flight"
|
|
flightdata "wucher/internal/domain/flight_data"
|
|
flightinspection "wucher/internal/domain/flight_inspection"
|
|
flightinspectionfilechecklist "wucher/internal/domain/flight_inspection_file_checklist"
|
|
flightprepcheck "wucher/internal/domain/flight_prep_check"
|
|
fmreport "wucher/internal/domain/fm_report"
|
|
"wucher/internal/domain/helicopter"
|
|
helicopterfile "wucher/internal/domain/helicopter_file"
|
|
"wucher/internal/domain/mission"
|
|
reserveac "wucher/internal/domain/reserve_ac"
|
|
"wucher/internal/service"
|
|
"wucher/internal/shared/pkg/apperrorsx"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
rootdto "wucher/internal/transport/http/dto"
|
|
requestdto "wucher/internal/transport/http/dto/request"
|
|
responsedto "wucher/internal/transport/http/dto/response"
|
|
shareddto "wucher/internal/transport/http/dto/shared"
|
|
"wucher/internal/transport/http/jsonapi"
|
|
"wucher/internal/transport/http/response"
|
|
"wucher/internal/transport/http/validators"
|
|
)
|
|
|
|
type ReserveAcHandler struct {
|
|
svc reserveac.ReserveAcService
|
|
auth *service.AuthService
|
|
flight flight.FlightService
|
|
flightData flightdata.Service
|
|
mission mission.Service
|
|
aircraft helicopter.Service
|
|
inspect flightinspection.FlightInspectionService
|
|
before beforeflightinspection.Service
|
|
prep flightprepcheck.Service
|
|
after afterflightinspection.Service
|
|
fmReport fmreport.Service
|
|
heliFile helicopterfile.Service
|
|
fileCk flightinspectionfilechecklist.Service
|
|
dutyRoster dutyroster.Service
|
|
storage fileManagerDownloadURLStorage
|
|
takeover *service.TakeoverService
|
|
validate *validators.Validator
|
|
}
|
|
|
|
func NewReserveAcHandler(svc reserveac.ReserveAcService) *ReserveAcHandler {
|
|
return &ReserveAcHandler{
|
|
svc: svc,
|
|
validate: validators.New(),
|
|
}
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFlightService(flightSvc flight.FlightService) *ReserveAcHandler {
|
|
h.flight = flightSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFlightDataService(flightDataSvc flightdata.Service) *ReserveAcHandler {
|
|
h.flightData = flightDataSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithMissionService(missionSvc mission.Service) *ReserveAcHandler {
|
|
h.mission = missionSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithAuthService(authSvc *service.AuthService) *ReserveAcHandler {
|
|
h.auth = authSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithHelicopterService(aircraftSvc helicopter.Service) *ReserveAcHandler {
|
|
h.aircraft = aircraftSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFlightInspectionService(inspectionSvc flightinspection.FlightInspectionService) *ReserveAcHandler {
|
|
h.inspect = inspectionSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithBeforeFlightInspectionService(beforeSvc beforeflightinspection.Service) *ReserveAcHandler {
|
|
h.before = beforeSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFlightPrepCheckService(prepSvc flightprepcheck.Service) *ReserveAcHandler {
|
|
h.prep = prepSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithAfterFlightInspectionService(afterSvc afterflightinspection.Service) *ReserveAcHandler {
|
|
h.after = afterSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFMReportService(reportSvc fmreport.Service) *ReserveAcHandler {
|
|
h.fmReport = reportSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithHelicopterFileService(heliFileSvc helicopterfile.Service) *ReserveAcHandler {
|
|
h.heliFile = heliFileSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFlightInspectionFileChecklistService(fileChecklistSvc flightinspectionfilechecklist.Service) *ReserveAcHandler {
|
|
h.fileCk = fileChecklistSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithDutyRosterService(dutyRosterSvc dutyroster.Service) *ReserveAcHandler {
|
|
h.dutyRoster = dutyRosterSvc
|
|
return h
|
|
}
|
|
|
|
func (h *ReserveAcHandler) WithFileStorage(storage fileManagerDownloadURLStorage) *ReserveAcHandler {
|
|
h.storage = storage
|
|
return h
|
|
}
|
|
|
|
func writeReserveAcErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error {
|
|
return response.WriteErrors(c, status, apperrorsx.EnrichErrorObjects(status, errs))
|
|
}
|
|
|
|
type reserveAcUpdateCoordinator interface {
|
|
UpdateDetailed(context.Context, service.ReserveAcUpdateInput) (*reserveac.ReserveAc, error)
|
|
}
|
|
|
|
type reserveAcCreateCoordinator interface {
|
|
CreateDetailed(context.Context, service.ReserveAcCreateInput) (*reserveac.ReserveAc, error)
|
|
}
|
|
|
|
func normalizeTakeoverDutyRosterDetail(resource *responsedto.DutyRosterResource) {
|
|
if resource == nil {
|
|
return
|
|
}
|
|
resource.Attributes.ShiftTime = responsedto.DutyRosterShiftTime{}
|
|
detail := &resource.Attributes.Detail
|
|
if detail.Pilot == nil {
|
|
detail.Pilot = []responsedto.DutyRosterPilotCrew{}
|
|
}
|
|
if detail.Doctor == nil {
|
|
detail.Doctor = []responsedto.DutyRosterCrew{}
|
|
}
|
|
if detail.Rescuer == nil {
|
|
detail.Rescuer = []responsedto.DutyRosterCrew{}
|
|
}
|
|
if detail.Helper == nil {
|
|
detail.Helper = []responsedto.DutyRosterCrew{}
|
|
}
|
|
if detail.OtherPerson == nil {
|
|
detail.OtherPerson = []responsedto.DutyRosterCrew{}
|
|
}
|
|
for i := range detail.Doctor {
|
|
detail.Doctor[i].ShiftTime = nil
|
|
}
|
|
for i := range detail.Rescuer {
|
|
detail.Rescuer[i].ShiftTime = nil
|
|
}
|
|
for i := range detail.Helper {
|
|
detail.Helper[i].ShiftTime = nil
|
|
}
|
|
for i := range detail.OtherPerson {
|
|
detail.OtherPerson[i].ShiftTime = nil
|
|
}
|
|
}
|
|
|
|
func parseTakeoverUUID(v string, pointer string) ([]byte, *jsonapi.ErrorObject) {
|
|
id, err := uuidv7.ParseString(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: strings.TrimSpace(pointer) + " is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func parseTakeoverDate(v string) (time.Time, *jsonapi.ErrorObject) {
|
|
d, err := service.ParseDutyRosterDateOnly(v)
|
|
if err != nil {
|
|
return time.Time{}, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "duty_date must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/duty_date"},
|
|
}
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// CreateReserveAc godoc
|
|
// @Summary Create reserve aircraft assignment
|
|
// @Description Create reserve AC, create draft inspection, fill before + prepare inspection payload, and assign to flight.
|
|
// @Tags Flights - Reserve AC
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.ReserveAcCreateRequest true "Create reserve AC request"
|
|
// @Success 201 {object} responsedto.ReserveAcResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/reserve-acs/create [post]
|
|
func (h *ReserveAcHandler) Create(c *fiber.Ctx) error {
|
|
var req requestdto.ReserveAcCreateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
|
|
helicopterID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.AircraftID))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
|
|
}})
|
|
}
|
|
|
|
inspectionDate, err := time.Parse("2006-01-02", strings.TrimSpace(req.Data.Attributes.Inspection.InspectionDate))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection.inspection_date must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/inspection/inspection_date"},
|
|
}})
|
|
}
|
|
if !hasReserveAcBeforePayload(req.Data.Attributes.Inspection.Before) {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection.before is required",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/inspection/before"},
|
|
}})
|
|
}
|
|
if !hasReserveAcPreparePayload(req.Data.Attributes.Inspection.Prepare) {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection.prepare is required",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/inspection/prepare"},
|
|
}})
|
|
}
|
|
flightID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.FlightID))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"},
|
|
}})
|
|
}
|
|
if creator, ok := h.svc.(reserveAcCreateCoordinator); ok {
|
|
created, createErr := creator.CreateDetailed(c.UserContext(), service.ReserveAcCreateInput{
|
|
HelicopterID: helicopterID,
|
|
FlightID: flightID,
|
|
InspectionDate: inspectionDate,
|
|
Before: &req.Data.Attributes.Inspection.Before,
|
|
Prepare: &req.Data.Attributes.Inspection.Prepare,
|
|
ActorUserID: actorUserID(c),
|
|
Flight: h.flight,
|
|
Inspect: h.inspect,
|
|
BeforeSvc: h.before,
|
|
Prep: h.prep,
|
|
HeliFile: h.heliFile,
|
|
FileChecklist: h.fileCk,
|
|
})
|
|
if createErr != nil {
|
|
return writeInternalHandlerErrorWithStatus(c, fiber.StatusBadRequest, "ReserveAcHandler.CreateDetailed", createErr)
|
|
}
|
|
return h.writeReserveAcCreateResponse(c, created, flightID)
|
|
}
|
|
|
|
if h.flight == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "flight service is not configured",
|
|
}})
|
|
}
|
|
if h.inspect == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.before == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "before flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.prep == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "flight prep check service is not configured",
|
|
}})
|
|
}
|
|
if h.heliFile == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "helicopter file service is not configured",
|
|
}})
|
|
}
|
|
if h.fileCk == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "flight inspection file checklist service is not configured",
|
|
}})
|
|
}
|
|
flightRow, err := h.flight.GetByID(c.UserContext(), flightID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if flightRow == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_id does not exist",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"},
|
|
}})
|
|
}
|
|
if len(flightRow.ReserveAcID) == 16 {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight already has reserve_ac assigned",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"},
|
|
}})
|
|
}
|
|
|
|
exists, err := h.svc.AircraftExists(c.UserContext(), helicopterID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if !exists {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_id does not exist",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
|
|
}})
|
|
}
|
|
|
|
inspection := &flightinspection.FlightInspection{
|
|
InspectionDate: inspectionDate.UTC(),
|
|
CreatedBy: actorUserID(c),
|
|
UpdatedBy: actorUserID(c),
|
|
}
|
|
if err := h.inspect.CreateDraft(c.UserContext(), inspection); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
row := &reserveac.ReserveAc{
|
|
Status: reserveac.StatusPending,
|
|
AircraftID: helicopterID,
|
|
InspectionID: inspection.ID,
|
|
CreatedBy: actorUserID(c),
|
|
UpdatedBy: actorUserID(c),
|
|
}
|
|
|
|
if err := h.svc.Create(c.UserContext(), row); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
beforeReq := &beforeflightinspection.UpsertRequest{
|
|
OilEngineNR1Checked: req.Data.Attributes.Inspection.Before.OilEngineNR1Checked,
|
|
OilEngineNR2Checked: req.Data.Attributes.Inspection.Before.OilEngineNR2Checked,
|
|
HydraulicLHChecked: req.Data.Attributes.Inspection.Before.HydraulicLHChecked,
|
|
HydraulicRHChecked: req.Data.Attributes.Inspection.Before.HydraulicRHChecked,
|
|
OilTransmissionMGBChecked: req.Data.Attributes.Inspection.Before.OilTransmissionMGBChecked,
|
|
OilTransmissionIGBChecked: req.Data.Attributes.Inspection.Before.OilTransmissionIGBChecked,
|
|
OilTransmissionTGBChecked: req.Data.Attributes.Inspection.Before.OilTransmissionTGBChecked,
|
|
FuelAmount: req.Data.Attributes.Inspection.Before.FuelAmount,
|
|
FuelUnit: req.Data.Attributes.Inspection.Before.FuelUnit,
|
|
FuelAddedAmount: req.Data.Attributes.Inspection.Before.FuelAddedAmount,
|
|
Note: req.Data.Attributes.Inspection.Before.Note,
|
|
}
|
|
if _, err := h.before.Upsert(c.UserContext(), inspection.ID, beforeReq); err != nil {
|
|
_ = h.svc.Delete(c.UserContext(), row.ID, actorUserID(c))
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if _, err := h.prep.InitializeChecks(c.UserContext(), inspection.ID); err != nil {
|
|
_ = h.svc.Delete(c.UserContext(), row.ID, actorUserID(c))
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if _, err := h.prep.UpsertByFlightInspection(c.UserContext(), inspection.ID, &flightprepcheck.UpsertRequest{
|
|
NOTAMBriefing: req.Data.Attributes.Inspection.Prepare.NotamBriefing,
|
|
WeatherBriefing: req.Data.Attributes.Inspection.Prepare.WeatherBriefing,
|
|
OperationalFlightPlan: req.Data.Attributes.Inspection.Prepare.OperationalFlightPlan,
|
|
}); err != nil {
|
|
_ = h.svc.Delete(c.UserContext(), row.ID, actorUserID(c))
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if ok, err := h.upsertInspectionFileChecklistSection(
|
|
c,
|
|
inspection.ID,
|
|
helicopterID,
|
|
helicopterfile.SectionBeforeFirstFlightInspection,
|
|
req.Data.Attributes.Inspection.Before.FileChecklist,
|
|
"/data/attributes/inspection/before/file_checklist",
|
|
row.ID,
|
|
); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return nil
|
|
}
|
|
if ok, err := h.upsertInspectionFileChecklistSection(
|
|
c,
|
|
inspection.ID,
|
|
helicopterID,
|
|
helicopterfile.SectionFlightPreparationAndWB,
|
|
req.Data.Attributes.Inspection.Prepare.FileChecklist,
|
|
"/data/attributes/inspection/prepare/file_checklist",
|
|
row.ID,
|
|
); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return nil
|
|
}
|
|
flightRow.ReserveAcID = append([]byte(nil), row.ID...)
|
|
flightRow.UpdatedBy = actorUserID(c)
|
|
if err := h.flight.Update(c.UserContext(), flightRow); err != nil {
|
|
_ = h.svc.Delete(c.UserContext(), row.ID, actorUserID(c))
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
inspectionSummary, _ := h.buildCreateInspectionSummary(c, inspection.ID, helicopterID)
|
|
row.Status = h.calculateReserveAcStatus(inspectionSummary)
|
|
if updateErr := h.svc.Update(c.UserContext(), row); updateErr != nil {
|
|
_ = h.svc.Delete(c.UserContext(), row.ID, actorUserID(c))
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
aircraftSummary, _ := h.buildAircraftSummary(c, helicopterID)
|
|
created, err := h.svc.GetByID(c.UserContext(), row.ID)
|
|
if err == nil && created != nil {
|
|
return response.Write(c, fiber.StatusCreated, reserveAcResource(created, reserveAcResourceOptions{
|
|
flightID: flightID,
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
}))
|
|
}
|
|
return response.Write(c, fiber.StatusCreated, reserveAcResource(row, reserveAcResourceOptions{
|
|
flightID: flightID,
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
}))
|
|
}
|
|
|
|
// GetReserveAc godoc
|
|
// @Summary Get reserve AC by UUID or flight UUID
|
|
// @Description Get reserve AC detail. Path `uuid` accepts reserve_ac id, and supports fallback to flight id.
|
|
// @Tags Flights - Reserve AC
|
|
// @Produce json
|
|
// @Param uuid path string true "Reserve AC UUID or Flight UUID"
|
|
// @Success 200 {object} responsedto.ReserveAcResponse
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/reserve-acs/get/{uuid} [get]
|
|
func (h *ReserveAcHandler) Get(c *fiber.Ctx) error {
|
|
idRaw := strings.TrimSpace(c.Params("id"))
|
|
if idRaw == "" {
|
|
idRaw = strings.TrimSpace(c.Params("uuid"))
|
|
}
|
|
id, err := uuidv7.ParseString(idRaw)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
|
|
}})
|
|
}
|
|
|
|
row, err := h.svc.GetByID(c.UserContext(), id)
|
|
if err != nil || row == nil {
|
|
if h.flight != nil {
|
|
flightRow, flightErr := h.flight.GetByID(c.UserContext(), id)
|
|
if flightErr == nil && flightRow != nil && len(flightRow.ReserveAcID) == 16 {
|
|
row, err = h.svc.GetByID(c.UserContext(), flightRow.ReserveAcID)
|
|
}
|
|
}
|
|
if err != nil || row == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "reserve_ac not found",
|
|
}})
|
|
}
|
|
}
|
|
|
|
inspectionSummary, _ := h.buildCreateInspectionSummary(c, row.InspectionID, row.AircraftID)
|
|
aircraftSummary, _ := h.buildAircraftSummary(c, row.AircraftID)
|
|
lastDaily, _ := h.svc.GetLastDailyInspectionByHelicopterID(c.UserContext(), row.AircraftID)
|
|
return response.Write(c, fiber.StatusOK, reserveAcResource(row, reserveAcResourceOptions{
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
lastDaily: reserveAcLastDailyInfo(lastDaily),
|
|
}))
|
|
}
|
|
|
|
// Legacy helper endpoint (no longer routed publicly).
|
|
func (h *ReserveAcHandler) GetAfterFlightInspection(c *fiber.Ctx) error {
|
|
inspectionID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("inspection_id")))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/inspection_id"},
|
|
}})
|
|
}
|
|
return h.getAfterFlightInspectionByID(c, inspectionID)
|
|
}
|
|
|
|
func (h *ReserveAcHandler) getAfterFlightInspectionByID(c *fiber.Ctx, inspectionID []byte) error {
|
|
if h.after == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Get failed",
|
|
Detail: "after flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.inspect == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Get failed",
|
|
Detail: "flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.heliFile == nil || h.fileCk == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Get failed",
|
|
Detail: "helicopter file checklist service is not configured",
|
|
}})
|
|
}
|
|
|
|
reserveAcRow, err := h.svc.GetActiveByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if reserveAcRow == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "reserve_ac not found for inspection_id",
|
|
}})
|
|
}
|
|
|
|
row, err := h.after.GetByFlightInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
if mapped := apperrorsx.Translate(apperrorsx.ModuleAfterFlightInspection, err); mapped != nil && mapped.Code != apperrorsx.CodeInternalServerError {
|
|
return apperrorsx.Write(c, mapped)
|
|
}
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
inspectionRow, _ := h.inspect.GetByID(c.UserContext(), inspectionID)
|
|
afterSection, err := h.buildInspectionChecklistSection(c, inspectionID, reserveAcRow.AircraftID, helicopterfile.SectionAfterLastFlightInspection)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Get failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
helicopterRow := h.afterFlightInspectionHelicopterResource(c.UserContext(), reserveAcRow)
|
|
summary := h.buildAfterFlightInspectionSummary(c.UserContext(), reserveAcRow)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(afterFlightInspectionPayload(inspectionID, inspectionRow, helicopterRow, row, afterSection, summary))
|
|
}
|
|
|
|
// Legacy helper endpoint (no longer routed publicly).
|
|
func (h *ReserveAcHandler) UpsertAfterFlightInspection(c *fiber.Ctx) error {
|
|
inspectionID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("inspection_id")))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/inspection_id"},
|
|
}})
|
|
}
|
|
|
|
var req requestdto.ReserveAcAfterFlightInspectionUpsertRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
return h.upsertAfterFlightInspectionByID(c, inspectionID, req.Data.Attributes)
|
|
}
|
|
|
|
func (h *ReserveAcHandler) upsertAfterFlightInspectionByID(c *fiber.Ctx, inspectionID []byte, attrs requestdto.ReserveAcAfterFlightInspectionAttributes) error {
|
|
if h.after == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "after flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.inspect == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.heliFile == nil || h.fileCk == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "helicopter file checklist service is not configured",
|
|
}})
|
|
}
|
|
|
|
reserveAcRow, err := h.svc.GetActiveByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if reserveAcRow == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "reserve_ac not found for inspection_id",
|
|
}})
|
|
}
|
|
|
|
if ok, detail, err := h.allFlightMissionsComplete(c.UserContext(), reserveAcRow); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
} else if !ok {
|
|
if strings.TrimSpace(detail) == "" {
|
|
detail = "all missions in this flight must have complete flight data before after flight inspection"
|
|
}
|
|
c.Locals(apperrorsx.ContextKeyHTTPErrorDetail, detail)
|
|
def := apperrorsx.ErrFlightDataAfterFlightIncomplete
|
|
return jsonapi.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: strconv.Itoa(def.Status),
|
|
Code: def.Code,
|
|
ErrorCode: def.ErrorCode,
|
|
Title: def.Title,
|
|
Detail: detail,
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_inspection_id"},
|
|
}})
|
|
}
|
|
|
|
if ok, err := h.validateInspectionChecklistSectionComplete(
|
|
c,
|
|
reserveAcRow.AircraftID,
|
|
helicopterfile.SectionAfterLastFlightInspection,
|
|
attrs.FileChecklist,
|
|
"/data/attributes/file_checklist",
|
|
); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return nil
|
|
}
|
|
|
|
upsertReq := &afterflightinspection.UpsertRequest{
|
|
OilEngineNR1Checked: attrs.OilEngineNR1Checked,
|
|
OilEngineNR2Checked: attrs.OilEngineNR2Checked,
|
|
HydraulicLHChecked: attrs.HydraulicLHChecked,
|
|
HydraulicRHChecked: attrs.HydraulicRHChecked,
|
|
OilTransmissionMGBChecked: attrs.OilTransmissionMGBChecked,
|
|
OilTransmissionIGBChecked: attrs.OilTransmissionIGBChecked,
|
|
OilTransmissionTGBChecked: attrs.OilTransmissionTGBChecked,
|
|
Note: attrs.Note,
|
|
}
|
|
row, err := h.after.Upsert(c.UserContext(), inspectionID, upsertReq)
|
|
if err != nil {
|
|
if mapped := apperrorsx.Translate(apperrorsx.ModuleAfterFlightInspection, err); mapped != nil && mapped.Code != apperrorsx.CodeInternalServerError {
|
|
return apperrorsx.Write(c, mapped)
|
|
}
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
if ok, err := h.upsertInspectionFileChecklistSection(
|
|
c,
|
|
inspectionID,
|
|
reserveAcRow.AircraftID,
|
|
helicopterfile.SectionAfterLastFlightInspection,
|
|
attrs.FileChecklist,
|
|
"/data/attributes/file_checklist",
|
|
nil,
|
|
); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return nil
|
|
}
|
|
|
|
if h.fmReport != nil {
|
|
flightRow, flightErr := h.flight.GetByReserveAcID(c.UserContext(), reserveAcRow.ID)
|
|
if flightErr != nil || flightRow == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Internal Server Error",
|
|
Detail: "failed to load flight for fm report",
|
|
}})
|
|
}
|
|
if _, reportErr := h.fmReport.UpsertForFlight(c.UserContext(), flightRow, inspectionID); reportErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Internal Server Error",
|
|
Detail: "failed to upsert fm report",
|
|
}})
|
|
}
|
|
}
|
|
|
|
// The after-flight inspection has already been persisted above; this only
|
|
// re-reads data to build the response. A failure here is a server-side
|
|
// rendering error, not a failed update, so return 500 (not 400).
|
|
inspectionRow, _ := h.inspect.GetByID(c.UserContext(), inspectionID)
|
|
afterSection, err := h.buildInspectionChecklistSection(c, inspectionID, reserveAcRow.AircraftID, helicopterfile.SectionAfterLastFlightInspection)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Internal Server Error",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
helicopterRow := h.afterFlightInspectionHelicopterResource(c.UserContext(), reserveAcRow)
|
|
summary := h.buildAfterFlightInspectionSummary(c.UserContext(), reserveAcRow)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(afterFlightInspectionPayload(inspectionID, inspectionRow, helicopterRow, row, afterSection, summary))
|
|
}
|
|
|
|
func (h *ReserveAcHandler) allFlightMissionsComplete(ctx context.Context, reserveAcRow *reserveac.ReserveAc) (bool, string, error) {
|
|
if h == nil || h.flight == nil || h.mission == nil || h.flightData == nil || reserveAcRow == nil || len(reserveAcRow.ID) != 16 {
|
|
return false, "", nil
|
|
}
|
|
|
|
flightRow, err := h.flight.GetByReserveAcID(ctx, reserveAcRow.ID)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
if flightRow == nil || len(flightRow.ID) != 16 {
|
|
return false, "", nil
|
|
}
|
|
|
|
missions, err := h.mission.ListByFlightID(ctx, flightRow.ID)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
if len(missions) == 0 {
|
|
return false, "cannot submit after-flight: this flight has no missions", nil
|
|
}
|
|
|
|
missing := make([]string, 0, len(missions))
|
|
for i := range missions {
|
|
fd, ok := h.loadMissionFlightData(ctx, missions[i])
|
|
if !ok || !flightdata.IsComplete(fd) {
|
|
missionID, _ := uuidv7.BytesToString(missions[i].ID)
|
|
label := "mission"
|
|
if missionID != "" {
|
|
label = "mission " + missionID
|
|
}
|
|
if t := strings.TrimSpace(missions[i].Type); t != "" {
|
|
label += " (" + strings.ToUpper(t) + ")"
|
|
}
|
|
if !ok || fd == nil {
|
|
label += " has no flight data"
|
|
} else {
|
|
label += " has incomplete flight data"
|
|
}
|
|
missing = append(missing, label)
|
|
}
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
return false, "cannot submit after-flight: " + strings.Join(missing, "; "), nil
|
|
}
|
|
|
|
return true, "", nil
|
|
}
|
|
|
|
// GetLastFlightsInspection godoc
|
|
// @Summary Get last flights inspection
|
|
// @Description Get after-flight inspection detail and AFI file checklist by query `inspection_id`.
|
|
// @Tags Flights - After Flights Inspection
|
|
// @Produce json
|
|
// @Param inspection_id query string true "Flight Inspection UUID"
|
|
// @Success 200 {object} rootdto.AfterFlightInspectionResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/after-flight-inspection/get [get]
|
|
func (h *ReserveAcHandler) GetLastFlightsInspection(c *fiber.Ctx) error {
|
|
inspectionID, err := uuidv7.ParseString(strings.TrimSpace(c.Query("inspection_id")))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/query/inspection_id"},
|
|
}})
|
|
}
|
|
return h.getAfterFlightInspectionByID(c, inspectionID)
|
|
}
|
|
|
|
// CreateLastFlightsInspection godoc
|
|
// @Summary Create last flights inspection
|
|
// @Description Create after-flight inspection and AFI file checklist for a flight inspection.
|
|
// @Tags Flights - After Flights Inspection
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.LastFlightsInspectionCreateRequest true "Create last flights inspection request"
|
|
// @Success 200 {object} rootdto.AfterFlightInspectionResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/after-flight-inspection/create [post]
|
|
func (h *ReserveAcHandler) CreateLastFlightsInspection(c *fiber.Ctx) error {
|
|
if h.after == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "after flight inspection service is not configured",
|
|
}})
|
|
}
|
|
var req requestdto.LastFlightsInspectionCreateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
inspectionID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.FlightInspectionID))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_inspection_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_inspection_id"},
|
|
}})
|
|
}
|
|
existing, err := h.after.GetByFlightInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if existing != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "after flight inspection already exists",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_inspection_id"},
|
|
}})
|
|
}
|
|
return h.upsertAfterFlightInspectionByID(c, inspectionID, req.Data.Attributes.ReserveAcAfterFlightInspectionAttributes)
|
|
}
|
|
|
|
// UpdateLastFlightsInspection godoc
|
|
// @Summary Update last flights inspection
|
|
// @Description Update after-flight inspection and AFI file checklist for a flight inspection.
|
|
// @Tags Flights - After Flights Inspection
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.LastFlightsInspectionUpdateRequest true "Update last flights inspection request"
|
|
// @Success 200 {object} rootdto.AfterFlightInspectionResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/after-flight-inspection/update [patch]
|
|
func (h *ReserveAcHandler) UpdateLastFlightsInspection(c *fiber.Ctx) error {
|
|
if h.after == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "after flight inspection service is not configured",
|
|
}})
|
|
}
|
|
var req requestdto.LastFlightsInspectionUpdateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
inspectionID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.FlightInspectionID))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_inspection_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_inspection_id"},
|
|
}})
|
|
}
|
|
existing, err := h.after.GetByFlightInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if existing == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "after flight inspection not found",
|
|
}})
|
|
}
|
|
return h.upsertAfterFlightInspectionByID(c, inspectionID, req.Data.Attributes.ReserveAcAfterFlightInspectionAttributes)
|
|
}
|
|
|
|
// DeleteLastFlightsInspection godoc
|
|
// @Summary Delete last flights inspection
|
|
// @Description Delete after-flight inspection and AFI checklist rows by `flight_inspection_id`.
|
|
// @Tags Flights - After Flights Inspection
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.LastFlightsInspectionDeleteRequest true "Delete last flights inspection request"
|
|
// @Success 200 {object} jsonapi.Document
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/after-flight-inspection/delete [delete]
|
|
func (h *ReserveAcHandler) DeleteLastFlightsInspection(c *fiber.Ctx) error {
|
|
var req requestdto.LastFlightsInspectionDeleteRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
inspectionID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.FlightInspectionID))
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_inspection_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_inspection_id"},
|
|
}})
|
|
}
|
|
if h.after == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Delete failed",
|
|
Detail: "after flight inspection service is not configured",
|
|
}})
|
|
}
|
|
if h.heliFile == nil || h.fileCk == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Delete failed",
|
|
Detail: "helicopter file checklist service is not configured",
|
|
}})
|
|
}
|
|
|
|
existing, err := h.after.GetByFlightInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Delete failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if existing == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "after flight inspection not found",
|
|
}})
|
|
}
|
|
|
|
reserveAcRow, err := h.svc.GetActiveByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Delete failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if reserveAcRow == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "reserve_ac not found for inspection_id",
|
|
}})
|
|
}
|
|
|
|
if err := h.after.DeleteByFlightInspectionID(c.UserContext(), inspectionID); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Delete failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if err := h.deleteInspectionFileChecklistSection(c, inspectionID, reserveAcRow.AircraftID, helicopterfile.SectionAfterLastFlightInspection); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Delete failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
return response.Write(c, fiber.StatusOK, jsonapi.Resource{
|
|
Type: "last_flights_inspection",
|
|
ID: idString(inspectionID),
|
|
Attributes: map[string]any{
|
|
"deleted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
// UpdateReserveAc godoc
|
|
// @Summary Update reserve AC
|
|
// @Description Update reserve AC using body `data.id`.
|
|
// @Tags Flights - Reserve AC
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.ReserveAcUpdateRequest true "Update reserve AC request"
|
|
// @Success 200 {object} responsedto.ReserveAcResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/reserve-acs/update [patch]
|
|
func (h *ReserveAcHandler) Update(c *fiber.Ctx) error {
|
|
var req requestdto.ReserveAcUpdateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
|
|
pathID := strings.TrimSpace(c.Params("id"))
|
|
if pathID == "" {
|
|
pathID = strings.TrimSpace(c.Params("uuid"))
|
|
}
|
|
bodyID := strings.TrimSpace(req.Data.ID)
|
|
if pathID == "" && bodyID == "" {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "id is required",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/id"},
|
|
}})
|
|
}
|
|
if pathID != "" && bodyID != "" && bodyID != pathID {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "id does not match path id",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/id"},
|
|
}})
|
|
}
|
|
targetID := bodyID
|
|
if targetID == "" {
|
|
targetID = pathID
|
|
}
|
|
id, err := uuidv7.ParseString(targetID)
|
|
if err != nil {
|
|
sourcePtr := "/data/id"
|
|
if targetID == pathID && pathID != "" && bodyID == "" {
|
|
sourcePtr = "/path/id"
|
|
}
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: sourcePtr},
|
|
}})
|
|
}
|
|
|
|
attrs := req.Data.Attributes
|
|
flightRaw, flightPointer, hasFlight := reserveAcUpdateFlightInput(attrs)
|
|
if attrs.AircraftID == nil && attrs.Inspection == nil && !hasFlight {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "at least one attribute must be provided",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"},
|
|
}})
|
|
}
|
|
|
|
if updater, ok := h.svc.(reserveAcUpdateCoordinator); ok {
|
|
updated, updateErr := updater.UpdateDetailed(c.UserContext(), service.ReserveAcUpdateInput{
|
|
ID: id,
|
|
ActorUserID: actorUserID(c),
|
|
AircraftID: attrs.AircraftID,
|
|
FlightID: attrs.FlightID,
|
|
Inspection: attrs.Inspection,
|
|
Flight: h.flight,
|
|
Inspect: h.inspect,
|
|
Before: h.before,
|
|
Prep: h.prep,
|
|
HeliFile: h.heliFile,
|
|
FileChecklist: h.fileCk,
|
|
})
|
|
if updateErr != nil {
|
|
return writeReserveAcUpdateError(c, updateErr)
|
|
}
|
|
return h.writeReserveAcUpdateResponse(c, updated)
|
|
}
|
|
|
|
existing, err := h.svc.GetByID(c.UserContext(), id)
|
|
if err != nil || existing == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "reserve_ac not found",
|
|
}})
|
|
}
|
|
|
|
if hasFlight {
|
|
if h.flight == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "flight service is not configured",
|
|
}})
|
|
}
|
|
flightID, parseErr := uuidv7.ParseString(flightRaw)
|
|
if parseErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: flightPointer},
|
|
}})
|
|
}
|
|
|
|
targetFlight, flightErr := h.flight.GetByID(c.UserContext(), flightID)
|
|
if flightErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: flightErr.Error(),
|
|
}})
|
|
}
|
|
if targetFlight == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight_id does not exist",
|
|
Source: &jsonapi.ErrorSource{Pointer: flightPointer},
|
|
}})
|
|
}
|
|
if len(targetFlight.ReserveAcID) == 16 && !bytes.Equal(targetFlight.ReserveAcID, existing.ID) {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "flight already has reserve_ac assigned",
|
|
Source: &jsonapi.ErrorSource{Pointer: flightPointer},
|
|
}})
|
|
}
|
|
|
|
currentFlight, currentErr := h.flight.GetByReserveAcID(c.UserContext(), existing.ID)
|
|
if currentErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: currentErr.Error(),
|
|
}})
|
|
}
|
|
if currentFlight != nil && !bytes.Equal(currentFlight.ID, targetFlight.ID) {
|
|
currentFlight.ReserveAcID = nil
|
|
currentFlight.UpdatedBy = actorUserID(c)
|
|
if updateErr := h.flight.Update(c.UserContext(), currentFlight); updateErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
}
|
|
|
|
if len(targetFlight.ReserveAcID) != 16 || !bytes.Equal(targetFlight.ReserveAcID, existing.ID) {
|
|
targetFlight.ReserveAcID = append([]byte(nil), existing.ID...)
|
|
targetFlight.UpdatedBy = actorUserID(c)
|
|
if updateErr := h.flight.Update(c.UserContext(), targetFlight); updateErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
}
|
|
}
|
|
|
|
if attrs.AircraftID != nil && strings.TrimSpace(*attrs.AircraftID) != "" {
|
|
aircraftValue := strings.TrimSpace(*attrs.AircraftID)
|
|
helicopterID, parseErr := uuidv7.ParseString(aircraftValue)
|
|
if parseErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
|
|
}})
|
|
}
|
|
exists, existsErr := h.svc.AircraftExists(c.UserContext(), helicopterID)
|
|
if existsErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: existsErr.Error(),
|
|
}})
|
|
}
|
|
if !exists {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_id does not exist",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
|
|
}})
|
|
}
|
|
existing.AircraftID = helicopterID
|
|
}
|
|
|
|
if attrs.Inspection != nil {
|
|
inspectionRow, getErr := h.inspect.GetByID(c.UserContext(), existing.InspectionID)
|
|
if getErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if inspectionRow == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "flight inspection not found",
|
|
}})
|
|
}
|
|
|
|
if attrs.Inspection.InspectionDate != nil {
|
|
inspectionDate, parseErr := time.Parse("2006-01-02", strings.TrimSpace(*attrs.Inspection.InspectionDate))
|
|
if parseErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "inspection.inspection_date must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/inspection/inspection_date"},
|
|
}})
|
|
}
|
|
inspectionRow.InspectionDate = inspectionDate.UTC()
|
|
inspectionRow.UpdatedBy = actorUserID(c)
|
|
if updateErr := h.inspect.Update(c.UserContext(), inspectionRow); updateErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
}
|
|
|
|
if attrs.Inspection.Before != nil {
|
|
beforeReq := &beforeflightinspection.UpsertRequest{
|
|
OilEngineNR1Checked: attrs.Inspection.Before.OilEngineNR1Checked,
|
|
OilEngineNR2Checked: attrs.Inspection.Before.OilEngineNR2Checked,
|
|
HydraulicLHChecked: attrs.Inspection.Before.HydraulicLHChecked,
|
|
HydraulicRHChecked: attrs.Inspection.Before.HydraulicRHChecked,
|
|
OilTransmissionMGBChecked: attrs.Inspection.Before.OilTransmissionMGBChecked,
|
|
OilTransmissionIGBChecked: attrs.Inspection.Before.OilTransmissionIGBChecked,
|
|
OilTransmissionTGBChecked: attrs.Inspection.Before.OilTransmissionTGBChecked,
|
|
FuelAmount: attrs.Inspection.Before.FuelAmount,
|
|
FuelUnit: attrs.Inspection.Before.FuelUnit,
|
|
FuelAddedAmount: attrs.Inspection.Before.FuelAddedAmount,
|
|
Note: attrs.Inspection.Before.Note,
|
|
}
|
|
if _, upsertErr := h.before.Upsert(c.UserContext(), existing.InspectionID, beforeReq); upsertErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: upsertErr.Error(),
|
|
}})
|
|
}
|
|
if attrs.Inspection.Before.FileChecklist != nil {
|
|
if ok, err := h.upsertInspectionFileChecklistSection(
|
|
c,
|
|
existing.InspectionID,
|
|
existing.AircraftID,
|
|
helicopterfile.SectionBeforeFirstFlightInspection,
|
|
attrs.Inspection.Before.FileChecklist,
|
|
"/data/attributes/inspection/before/file_checklist",
|
|
nil,
|
|
); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if attrs.Inspection.Prepare != nil {
|
|
if _, upsertErr := h.prep.UpsertByFlightInspection(c.UserContext(), existing.InspectionID, &flightprepcheck.UpsertRequest{
|
|
NOTAMBriefing: attrs.Inspection.Prepare.NotamBriefing,
|
|
WeatherBriefing: attrs.Inspection.Prepare.WeatherBriefing,
|
|
OperationalFlightPlan: attrs.Inspection.Prepare.OperationalFlightPlan,
|
|
}); upsertErr != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: upsertErr.Error(),
|
|
}})
|
|
}
|
|
|
|
if attrs.Inspection.Prepare.FileChecklist != nil {
|
|
if ok, err := h.upsertInspectionFileChecklistSection(
|
|
c,
|
|
existing.InspectionID,
|
|
existing.AircraftID,
|
|
helicopterfile.SectionFlightPreparationAndWB,
|
|
attrs.Inspection.Prepare.FileChecklist,
|
|
"/data/attributes/inspection/prepare/file_checklist",
|
|
nil,
|
|
); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
existing.UpdatedBy = actorUserID(c)
|
|
updatedInspectionSummary, _ := h.buildCreateInspectionSummary(c, existing.InspectionID, existing.AircraftID)
|
|
existing.Status = h.calculateReserveAcStatus(updatedInspectionSummary)
|
|
if err := h.svc.Update(c.UserContext(), existing); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
updated, err := h.svc.GetByID(c.UserContext(), id)
|
|
if err == nil && updated != nil {
|
|
inspectionSummary, _ := h.buildCreateInspectionSummary(c, updated.InspectionID, updated.AircraftID)
|
|
aircraftSummary, _ := h.buildAircraftSummary(c, updated.AircraftID)
|
|
return response.Write(c, fiber.StatusOK, reserveAcResource(updated, reserveAcResourceOptions{
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
}))
|
|
}
|
|
inspectionSummary, _ := h.buildCreateInspectionSummary(c, existing.InspectionID, existing.AircraftID)
|
|
aircraftSummary, _ := h.buildAircraftSummary(c, existing.AircraftID)
|
|
return response.Write(c, fiber.StatusOK, reserveAcResource(existing, reserveAcResourceOptions{
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
}))
|
|
}
|
|
|
|
func (h *ReserveAcHandler) writeReserveAcUpdateResponse(c *fiber.Ctx, row *reserveac.ReserveAc) error {
|
|
if row == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "reserve_ac update returned no row",
|
|
}})
|
|
}
|
|
|
|
inspectionSummary, _ := h.buildCreateInspectionSummary(c, row.InspectionID, row.AircraftID)
|
|
aircraftSummary, _ := h.buildAircraftSummary(c, row.AircraftID)
|
|
return response.Write(c, fiber.StatusOK, reserveAcResource(row, reserveAcResourceOptions{
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
}))
|
|
}
|
|
|
|
func (h *ReserveAcHandler) writeReserveAcCreateResponse(c *fiber.Ctx, row *reserveac.ReserveAc, flightID []byte) error {
|
|
if row == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "reserve_ac create returned no row",
|
|
}})
|
|
}
|
|
|
|
inspectionSummary, _ := h.buildCreateInspectionSummary(c, row.InspectionID, row.AircraftID)
|
|
aircraftSummary, _ := h.buildAircraftSummary(c, row.AircraftID)
|
|
return response.Write(c, fiber.StatusCreated, reserveAcResource(row, reserveAcResourceOptions{
|
|
flightID: flightID,
|
|
inspection: inspectionSummary,
|
|
aircraft: aircraftSummary,
|
|
}))
|
|
}
|
|
|
|
func writeReserveAcUpdateError(c *fiber.Ctx, err error) error {
|
|
if err == nil {
|
|
return writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "reserve_ac update failed",
|
|
}})
|
|
}
|
|
|
|
var updateErr *service.ReserveAcUpdateError
|
|
if !errors.As(err, &updateErr) {
|
|
return writeInternalHandlerErrorWithStatus(c, fiber.StatusBadRequest, "ReserveAcHandler.Update", err)
|
|
}
|
|
|
|
obj := jsonapi.ErrorObject{
|
|
Status: strconv.Itoa(updateErr.Status),
|
|
Title: updateErr.Title,
|
|
Detail: updateErr.Detail,
|
|
}
|
|
if updateErr.Pointer != "" {
|
|
obj.Source = &jsonapi.ErrorSource{Pointer: updateErr.Pointer}
|
|
}
|
|
return writeReserveAcErrors(c, updateErr.Status, []jsonapi.ErrorObject{obj})
|
|
}
|
|
|
|
// DeleteReserveAc godoc
|
|
// @Summary Delete reserve AC
|
|
// @Description Delete reserve AC by body `data.id`.
|
|
// @Tags Flights - Reserve AC
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body requestdto.ReserveAcDeleteRequest true "Delete reserve AC request"
|
|
// @Success 200 {object} jsonapi.Document
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 422 {object} jsonapi.ErrorDocument
|
|
// @Failure 404 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/reserve-acs/delete [delete]
|
|
func (h *ReserveAcHandler) Delete(c *fiber.Ctx) error {
|
|
var req requestdto.ReserveAcDeleteRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
if errs := h.validate.ValidateStruct(req); errs != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, errs)
|
|
}
|
|
|
|
idRaw := strings.TrimSpace(req.Data.ID)
|
|
id, err := uuidv7.ParseString(idRaw)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/id"},
|
|
}})
|
|
}
|
|
|
|
if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil {
|
|
status, errObj := buildServiceErrorObject("Delete failed", err, "reserve ac not found")
|
|
return writeReserveAcErrors(c, status, []jsonapi.ErrorObject{errObj})
|
|
}
|
|
|
|
return response.Write(c, fiber.StatusOK, jsonapi.Resource{
|
|
Type: "reserve_ac_delete",
|
|
Attributes: map[string]any{
|
|
"deleted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ListReserveAc godoc
|
|
// @Summary List reserve AC
|
|
// @Description Supports `search`, `filter[search]`, `sort`, `page[number]`, `page[size]`, `page`, `size`, and `limit`.
|
|
// @Tags Flights - Reserve AC
|
|
// @Produce json
|
|
// @Param search query string false "Search text"
|
|
// @Param filter[search] query string false "Search text (JSON:API style)"
|
|
// @Param id query string false "Reserve AC UUID (exact match)"
|
|
// @Param flight_id query string false "Flight UUID (exact match)"
|
|
// @Param sort query string false "Sort field"
|
|
// @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.ReserveAcListResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/reserve-acs/get-all [get]
|
|
func (h *ReserveAcHandler) List(c *fiber.Ctx) error {
|
|
userID := actorUserID(c)
|
|
if len(userID) == 0 {
|
|
return writeReserveAcErrors(c, fiber.StatusUnauthorized, []jsonapi.ErrorObject{{
|
|
Status: "401",
|
|
Title: "Unauthorized",
|
|
Detail: "missing auth context",
|
|
}})
|
|
}
|
|
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
|
|
search := strings.TrimSpace(c.Query("filter[search]"))
|
|
if search == "" {
|
|
search = strings.TrimSpace(c.Query("search"))
|
|
}
|
|
from := strings.TrimSpace(c.Query("from"))
|
|
to := strings.TrimSpace(c.Query("to"))
|
|
if from == "" {
|
|
from = time.Now().UTC().Format("2006-01-02")
|
|
}
|
|
if to == "" {
|
|
to = from
|
|
}
|
|
|
|
groups, total, booked, err := h.buildReserveAcHelicopterGroups(c.UserContext(), userID, search, from, to, 0, 0)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
return response.WriteWithMeta(c, fiber.StatusOK, groups, map[string]any{
|
|
"page_number": pageNumber,
|
|
"page_size": pageSize,
|
|
"total": total,
|
|
"from": from,
|
|
"to": to,
|
|
"timezone": "UTC",
|
|
"booked_count": booked,
|
|
"available_count": total - booked,
|
|
"total_helicopters": total,
|
|
})
|
|
}
|
|
|
|
// ListReserveAcDatatable godoc
|
|
// @Summary List reserve AC (datatable)
|
|
// @Description Datatable list scoped by auth user. Admin can see all rows; non-admin only sees reserve AC linked to own flight assignments.
|
|
// @Tags Flights - Reserve AC
|
|
// @Produce json
|
|
// @Param search query string false "Search text"
|
|
// @Param filter[search] query string false "Search text (JSON:API style)"
|
|
// @Param id query string false "Reserve AC UUID (exact match)"
|
|
// @Param flight_id query string false "Flight UUID (exact match)"
|
|
// @Param sort query string false "Sort field"
|
|
// @Param page query int false "Page number (default 1)"
|
|
// @Param size query int false "Page size (default 20, max 100)"
|
|
// @Param limit query int false "Page size override"
|
|
// @Param draw query int false "Draw counter (optional)"
|
|
// @Success 200 {object} responsedto.ReserveAcListResponse
|
|
// @Failure 400 {object} jsonapi.ErrorDocument
|
|
// @Failure 401 {object} jsonapi.ErrorDocument
|
|
// @Router /api/v1/reserve-acs/get-all/dt [get]
|
|
func (h *ReserveAcHandler) ListDatatable(c *fiber.Ctx) error {
|
|
userID := actorUserID(c)
|
|
if len(userID) == 0 {
|
|
return writeReserveAcErrors(c, fiber.StatusUnauthorized, []jsonapi.ErrorObject{{
|
|
Status: "401",
|
|
Title: "Unauthorized",
|
|
Detail: "missing auth context",
|
|
}})
|
|
}
|
|
pageNumber := parseIntDefault(c.Query("page"), 1)
|
|
if pageNumber < 1 {
|
|
pageNumber = 1
|
|
}
|
|
pageSize := parseIntDefault(c.Query("limit"), 0)
|
|
if pageSize <= 0 {
|
|
pageSize = parseIntDefault(c.Query("size"), 20)
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
offset := (pageNumber - 1) * pageSize
|
|
_ = offset
|
|
draw := parseIntDefault(c.Query("draw"), pageNumber)
|
|
search := strings.TrimSpace(c.Query("filter[search]"))
|
|
if search == "" {
|
|
search = strings.TrimSpace(c.Query("search"))
|
|
}
|
|
from := strings.TrimSpace(c.Query("from"))
|
|
to := strings.TrimSpace(c.Query("to"))
|
|
if from == "" {
|
|
from = time.Now().UTC().Format("2006-01-02")
|
|
}
|
|
if to == "" {
|
|
to = from
|
|
}
|
|
groups, total, booked, err := h.buildReserveAcHelicopterGroups(c.UserContext(), userID, search, from, to, 0, 0)
|
|
if err != nil {
|
|
return writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: "an internal error occurred",
|
|
}})
|
|
}
|
|
|
|
return response.WriteWithMeta(c, fiber.StatusOK, groups, map[string]any{
|
|
"draw": draw,
|
|
"records_total": total,
|
|
"records_filtered": total,
|
|
"page_number": pageNumber,
|
|
"page_size": pageSize,
|
|
"total": total,
|
|
"from": from,
|
|
"to": to,
|
|
"timezone": "UTC",
|
|
"booked_count": booked,
|
|
"available_count": total - booked,
|
|
"total_helicopters": total,
|
|
})
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildReserveAcHelicopterGroups(ctx context.Context, userID []byte, search, fromRaw, toRaw string, limit, offset int) ([]responsedto.ReserveAcHelicopterGroupResource, int64, int64, error) {
|
|
if h.aircraft == nil || h.flight == nil {
|
|
return []responsedto.ReserveAcHelicopterGroupResource{}, 0, 0, nil
|
|
}
|
|
from, err := time.Parse("2006-01-02", fromRaw)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
to, err := time.Parse("2006-01-02", toRaw)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
if to.Before(from) {
|
|
to = from
|
|
}
|
|
|
|
helicopters, _, err := h.aircraft.List(ctx, "", nil, "", 0, 0, nil)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
flights := make([]flight.Flight, 0)
|
|
if h.isReserveAcAdmin(ctx, userID) {
|
|
flights, _, err = h.flight.List(ctx, "", "", "flights.date DESC", 0, 0)
|
|
} else {
|
|
flights, _, err = h.flight.ListByUserID(ctx, userID, 0, 0)
|
|
}
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
search = strings.ToLower(strings.TrimSpace(search))
|
|
flightByHelicopter := make(map[string]flight.Flight, len(flights))
|
|
for i := range flights {
|
|
if flights[i].Date.Before(from) || flights[i].Date.After(to.Add(24*time.Hour-time.Nanosecond)) {
|
|
continue
|
|
}
|
|
heliKey := idString(flights[i].TakeoverAcID)
|
|
if heliKey == "" {
|
|
continue
|
|
}
|
|
existing, ok := flightByHelicopter[heliKey]
|
|
if !ok || flights[i].Date.After(existing.Date) {
|
|
flightByHelicopter[heliKey] = flights[i]
|
|
}
|
|
}
|
|
|
|
groups := make([]responsedto.ReserveAcHelicopterGroupResource, 0, len(helicopters))
|
|
var bookedCount int64
|
|
for i := range helicopters {
|
|
heli := helicopters[i]
|
|
if search != "" {
|
|
hay := strings.ToLower(strings.TrimSpace(heli.Designation + " " + heli.Identifier + " " + heli.Type))
|
|
if !strings.Contains(hay, search) {
|
|
continue
|
|
}
|
|
}
|
|
heliID := idString(heli.ID)
|
|
flightRow, booked := flightByHelicopter[heliID]
|
|
if booked {
|
|
bookedCount++
|
|
}
|
|
|
|
reserveID := ""
|
|
status := "available"
|
|
bookedAt := any(nil)
|
|
var bookedBy map[string]any
|
|
var bfiFields *beforeflightinspection.BeforeFlightInspection
|
|
var fpwbFields *flightprepcheck.FlightPrepCheck
|
|
var reserveRow *reserveac.ReserveAc
|
|
if booked {
|
|
status = "booked"
|
|
reserveID = idString(flightRow.ReserveAcID)
|
|
if len(flightRow.ReserveAcID) == 16 {
|
|
if currentReserve, getErr := h.svc.GetByID(ctx, flightRow.ReserveAcID); getErr == nil && currentReserve != nil {
|
|
reserveRow = currentReserve
|
|
bookedAt = reserveRow.UpdatedAt.UTC().Format(time.RFC3339)
|
|
if h.before != nil {
|
|
bfiFields, _ = h.before.GetByFlightInspectionID(ctx, reserveRow.InspectionID)
|
|
}
|
|
if h.prep != nil {
|
|
fpwbFields, _ = h.prep.GetByFlightInspection(ctx, reserveRow.InspectionID)
|
|
}
|
|
if name := h.resolveReserveAcUserName(ctx, reserveRow.UpdatedBy); name != "" {
|
|
bookedBy = map[string]any{"id": idString(reserveRow.UpdatedBy), "name": name}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if heli.AirOnGround {
|
|
status = "aog"
|
|
}
|
|
|
|
lastDailyRaw, _ := h.svc.GetLastDailyInspectionByHelicopterID(ctx, heli.ID)
|
|
lastDaily := responsedto.ReserveAcLastDailyInspectionDTO{
|
|
ID: nil,
|
|
InspectionBy: responsedto.ReserveAcInspectionBySummary{
|
|
ID: nil,
|
|
Name: "",
|
|
ShortName: "",
|
|
},
|
|
InspectionAt: nil,
|
|
}
|
|
if lastDailyRaw != nil {
|
|
lastDaily.ID = nullIfEmpty(idString(flightRow.ReserveAcID))
|
|
lastDaily.InspectionBy.Name = strings.TrimSpace(lastDailyRaw.Name)
|
|
lastDaily.InspectionBy.ShortName = strings.TrimSpace(lastDailyRaw.ShortName)
|
|
lastDaily.InspectionAt = lastDailyRaw.At.UTC().Format(time.RFC3339)
|
|
} else if reserveRow != nil {
|
|
// Fallback: if this is the first reserve for this helicopter, expose reserve actor/time as last inspection baseline.
|
|
lastDaily.ID = nullIfEmpty(idString(reserveRow.ID))
|
|
lastDaily.InspectionBy.ID = nullIfEmpty(idString(reserveRow.UpdatedBy))
|
|
if userName := h.resolveReserveAcUserName(ctx, reserveRow.UpdatedBy); userName != "" {
|
|
lastDaily.InspectionBy.Name = userName
|
|
lastDaily.InspectionBy.ShortName = shortNameFromFullName(userName)
|
|
}
|
|
lastDaily.InspectionAt = reserveRow.UpdatedAt.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
var bookingDetails *responsedto.ReserveAcBookingDetailsDTO
|
|
if booked {
|
|
bookingDetails = &responsedto.ReserveAcBookingDetailsDTO{
|
|
ReserveAcID: nullIfEmpty(reserveID),
|
|
FlightID: nullIfEmpty(idString(flightRow.ID)),
|
|
MissionCode: nullIfEmpty(ptrStringOrEmpty(flightRow.MissionCode)),
|
|
DutyDate: nullIfEmpty(dateStringOrEmpty(flightRow.Date)),
|
|
BookedAt: bookedAt,
|
|
BookedBy: bookedBy,
|
|
}
|
|
}
|
|
|
|
bfi := h.buildReserveAcHelicopterFileItems(ctx, heli.ID, helicopterfile.SectionBeforeFirstFlightInspection)
|
|
fpwb := h.buildReserveAcHelicopterFileItems(ctx, heli.ID, helicopterfile.SectionFlightPreparationAndWB)
|
|
|
|
groups = append(groups, responsedto.ReserveAcHelicopterGroupResource{
|
|
Type: "helicopter_reserve_group",
|
|
Attributes: responsedto.ReserveAcHelicopterGroupAttributes{
|
|
Helicopter: responsedto.ReserveAcHelicopterIdentity{
|
|
ID: heliID,
|
|
Designation: strings.TrimSpace(heli.Designation),
|
|
Identifier: strings.TrimSpace(heli.Identifier),
|
|
Type: strings.TrimSpace(heli.Type),
|
|
Status: status,
|
|
BookingDetails: bookingDetails,
|
|
},
|
|
LastDailyInspection: lastDaily,
|
|
BFI: reserveAcBFISectionDTO(bfiFields, bfi),
|
|
FPWB: reserveAcFPWBSectionDTO(fpwbFields, fpwb),
|
|
},
|
|
})
|
|
}
|
|
|
|
total := int64(len(groups))
|
|
paged := groups
|
|
if limit > 0 {
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset >= len(groups) {
|
|
paged = []responsedto.ReserveAcHelicopterGroupResource{}
|
|
} else {
|
|
end := offset + limit
|
|
if end > len(groups) {
|
|
end = len(groups)
|
|
}
|
|
paged = groups[offset:end]
|
|
}
|
|
}
|
|
return paged, total, bookedCount, nil
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildReserveAcHelicopterFileItems(ctx context.Context, helicopterID []byte, section string) []responsedto.ReserveAcHelicopterFileItem {
|
|
if h.heliFile == nil {
|
|
return []responsedto.ReserveAcHelicopterFileItem{}
|
|
}
|
|
files, err := h.heliFile.ListByHelicopterAndSection(ctx, helicopterID, section)
|
|
if err != nil {
|
|
return []responsedto.ReserveAcHelicopterFileItem{}
|
|
}
|
|
out := make([]responsedto.ReserveAcHelicopterFileItem, 0, len(files))
|
|
for i := range files {
|
|
fileName := ""
|
|
url := ""
|
|
if files[i].FileAttachment != nil && files[i].FileAttachment.File != nil {
|
|
fileName = strings.TrimSpace(files[i].FileAttachment.File.Name)
|
|
url = resolveFileManagerDownloadURL(ctx, h.storage, files[i].FileAttachment.File)
|
|
}
|
|
out = append(out, responsedto.ReserveAcHelicopterFileItem{
|
|
ID: idString(files[i].ID),
|
|
FileName: fileName,
|
|
URL: url,
|
|
UploadedAt: files[i].UpdatedAt.UTC().Format(time.RFC3339),
|
|
UploadedBy: idString(files[i].UpdatedBy),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (h *ReserveAcHandler) resolveReserveAcUserName(ctx context.Context, userID []byte) string {
|
|
if h.auth == nil || len(userID) != 16 {
|
|
return ""
|
|
}
|
|
u, err := h.auth.GetUserByID(ctx, userID)
|
|
if err != nil || u == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(strings.TrimSpace(u.FirstName) + " " + strings.TrimSpace(u.LastName))
|
|
}
|
|
|
|
func nullIfEmpty(v string) any {
|
|
if strings.TrimSpace(v) == "" {
|
|
return nil
|
|
}
|
|
return v
|
|
}
|
|
|
|
func ptrStringOrEmpty(v *string) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(*v)
|
|
}
|
|
|
|
func dateStringOrEmpty(v time.Time) string {
|
|
if v.IsZero() {
|
|
return ""
|
|
}
|
|
return v.UTC().Format("2006-01-02")
|
|
}
|
|
|
|
func shortNameFromFullName(fullName string) string {
|
|
parts := strings.Fields(strings.TrimSpace(fullName))
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
if len(parts) == 1 {
|
|
r := []rune(parts[0])
|
|
if len(r) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(string(r[0]))
|
|
}
|
|
first := []rune(parts[0])
|
|
last := []rune(parts[len(parts)-1])
|
|
if len(first) == 0 || len(last) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(string(first[0]) + string(last[0]))
|
|
}
|
|
|
|
func reserveAcBFISectionDTO(v *beforeflightinspection.BeforeFlightInspection, files []responsedto.ReserveAcHelicopterFileItem) responsedto.ReserveAcBFISection {
|
|
out := responsedto.ReserveAcBFISection{Files: files}
|
|
if v == nil {
|
|
return out
|
|
}
|
|
out.FuelAddedAmount = v.FuelAddedAmount
|
|
out.FuelAmount = v.FuelAmount
|
|
out.FuelUnit = strings.TrimSpace(v.FuelUnit)
|
|
out.HydraulicLHChecked = v.HydraulicLHChecked
|
|
out.HydraulicRHChecked = v.HydraulicRHChecked
|
|
out.OilEngineNR1Checked = v.OilEngineNR1Checked
|
|
out.OilEngineNR2Checked = v.OilEngineNR2Checked
|
|
out.OilTransmissionIGBChecked = v.OilTransmissionIGBChecked
|
|
out.OilTransmissionMGBChecked = v.OilTransmissionMGBChecked
|
|
out.OilTransmissionTGBChecked = v.OilTransmissionTGBChecked
|
|
return out
|
|
}
|
|
|
|
func reserveAcFPWBSectionDTO(v *flightprepcheck.FlightPrepCheck, files []responsedto.ReserveAcHelicopterFileItem) responsedto.ReserveAcFPWBSection {
|
|
out := responsedto.ReserveAcFPWBSection{Files: files}
|
|
if v == nil {
|
|
return out
|
|
}
|
|
out.NotamBriefing = boolPtr(v.NOTAMBriefing)
|
|
out.WeatherBriefing = boolPtr(v.WeatherBriefing)
|
|
out.OperationalFlightPlan = boolPtr(v.OperationalFlightPlan)
|
|
return out
|
|
}
|
|
|
|
func (h *ReserveAcHandler) isReserveAcAdmin(ctx context.Context, userID []byte) bool {
|
|
if h.auth == nil || len(userID) != 16 {
|
|
return false
|
|
}
|
|
user, err := h.auth.GetUserByID(ctx, userID)
|
|
if err != nil || user == nil {
|
|
return false
|
|
}
|
|
roleIDs := make([][]byte, 0, 1+len(user.RoleIDs))
|
|
if len(user.RoleID) == 16 {
|
|
roleIDs = append(roleIDs, user.RoleID)
|
|
}
|
|
roleIDs = append(roleIDs, user.RoleIDs...)
|
|
seen := make(map[string]struct{}, len(roleIDs))
|
|
for i := range roleIDs {
|
|
if len(roleIDs[i]) != 16 {
|
|
continue
|
|
}
|
|
key := idString(roleIDs[i])
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
role, err := h.auth.GetRoleByID(ctx, roleIDs[i])
|
|
if err != nil || role == nil {
|
|
continue
|
|
}
|
|
code := strings.ToLower(strings.TrimSpace(role.Code))
|
|
name := strings.ToLower(strings.TrimSpace(role.Name))
|
|
if code == "admin" || name == "admin" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type reserveAcListItem struct {
|
|
row reserveac.ReserveAc
|
|
flightID []byte
|
|
placeholder bool
|
|
}
|
|
|
|
func (h *ReserveAcHandler) listReserveAcByAuthUser(ctx context.Context, userID []byte, filter, sortParam string, limit, offset int, idFilterKey, flightIDFilterKey string) ([]reserveAcListItem, int64, error) {
|
|
if h.flight == nil || len(userID) != 16 {
|
|
return []reserveAcListItem{}, 0, nil
|
|
}
|
|
flights, _, err := h.flight.ListByUserID(ctx, userID, 100, 0)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items := h.buildReserveAcListItemsFromFlights(ctx, flights)
|
|
items = filterReserveAcListItemsByExact(items, idFilterKey, flightIDFilterKey)
|
|
items = filterReserveAcListItemsBySearch(items, filter)
|
|
sortReserveAcListItems(items, sortParam)
|
|
total := int64(len(items))
|
|
return paginateReserveAcListItems(items, limit, offset), total, nil
|
|
}
|
|
|
|
func (h *ReserveAcHandler) collectReserveAcRowsByScope(
|
|
ctx context.Context,
|
|
userID []byte,
|
|
isAdmin bool,
|
|
filter string,
|
|
sortParam string,
|
|
limit int,
|
|
offset int,
|
|
idFilter []byte,
|
|
idFilterKey string,
|
|
flightIDFilter []byte,
|
|
flightIDFilterKey string,
|
|
) ([]reserveAcListItem, int64, error) {
|
|
_ = idFilter
|
|
_ = flightIDFilter
|
|
if isAdmin {
|
|
if h.flight == nil {
|
|
return []reserveAcListItem{}, 0, nil
|
|
}
|
|
flights, _, err := h.flight.List(ctx, "", "", "flights.date DESC", 0, 0)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items := h.buildReserveAcListItemsFromFlights(ctx, flights)
|
|
items = filterReserveAcListItemsByExact(items, idFilterKey, flightIDFilterKey)
|
|
items = filterReserveAcListItemsBySearch(items, filter)
|
|
sortReserveAcListItems(items, sortParam)
|
|
total := int64(len(items))
|
|
return paginateReserveAcListItems(items, limit, offset), total, nil
|
|
}
|
|
return h.listReserveAcByAuthUser(ctx, userID, filter, sortParam, limit, offset, idFilterKey, flightIDFilterKey)
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildReserveAcListItemsFromFlights(ctx context.Context, flights []flight.Flight) []reserveAcListItem {
|
|
items := make([]reserveAcListItem, 0, len(flights))
|
|
seenReserve := make(map[string]struct{}, len(flights))
|
|
for i := range flights {
|
|
flightID := append([]byte(nil), flights[i].ID...)
|
|
if len(flights[i].ReserveAcID) == 16 {
|
|
key := idString(flights[i].ReserveAcID)
|
|
if key != "" {
|
|
if _, ok := seenReserve[key]; ok {
|
|
continue
|
|
}
|
|
seenReserve[key] = struct{}{}
|
|
if row, err := h.svc.GetByID(ctx, flights[i].ReserveAcID); err == nil && row != nil {
|
|
items = append(items, reserveAcListItem{row: *row, flightID: flightID})
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
items = append(items, reserveAcListItem{
|
|
row: reserveac.ReserveAc{
|
|
Status: reserveac.StatusPending,
|
|
AircraftID: append([]byte(nil), flights[i].ReserveAcID...),
|
|
},
|
|
flightID: flightID,
|
|
placeholder: true,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func filterReserveAcListItemsByExact(items []reserveAcListItem, reserveIDFilterKey, flightIDFilterKey string) []reserveAcListItem {
|
|
if reserveIDFilterKey == "" && flightIDFilterKey == "" {
|
|
return items
|
|
}
|
|
out := make([]reserveAcListItem, 0, len(items))
|
|
for i := range items {
|
|
if reserveIDFilterKey != "" && idString(items[i].row.ID) != reserveIDFilterKey {
|
|
continue
|
|
}
|
|
if flightIDFilterKey != "" && idString(items[i].flightID) != flightIDFilterKey {
|
|
continue
|
|
}
|
|
out = append(out, items[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func filterReserveAcListItemsBySearch(items []reserveAcListItem, filter string) []reserveAcListItem {
|
|
filter = strings.ToLower(strings.TrimSpace(filter))
|
|
if filter == "" {
|
|
return items
|
|
}
|
|
out := make([]reserveAcListItem, 0, len(items))
|
|
for i := range items {
|
|
if reserveAcMatchFilter(items[i].row, idString(items[i].flightID), filter) {
|
|
out = append(out, items[i])
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func paginateReserveAcListItems(items []reserveAcListItem, limit, offset int) []reserveAcListItem {
|
|
if limit <= 0 {
|
|
return items
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset >= len(items) {
|
|
return []reserveAcListItem{}
|
|
}
|
|
end := offset + limit
|
|
if end > len(items) {
|
|
end = len(items)
|
|
}
|
|
return items[offset:end]
|
|
}
|
|
|
|
func sortReserveAcListItems(items []reserveAcListItem, sortParam string) {
|
|
field := strings.TrimSpace(sortParam)
|
|
desc := false
|
|
if strings.HasPrefix(field, "-") {
|
|
desc = true
|
|
field = strings.TrimPrefix(field, "-")
|
|
}
|
|
switch field {
|
|
case "status", "created_at", "updated_at":
|
|
default:
|
|
field = "created_at"
|
|
desc = true
|
|
}
|
|
sort.SliceStable(items, func(i, j int) bool {
|
|
left := items[i].row
|
|
right := items[j].row
|
|
cmp := 0
|
|
switch field {
|
|
case "status":
|
|
ls := strings.ToLower(strings.TrimSpace(left.Status))
|
|
rs := strings.ToLower(strings.TrimSpace(right.Status))
|
|
switch {
|
|
case ls < rs:
|
|
cmp = -1
|
|
case ls > rs:
|
|
cmp = 1
|
|
default:
|
|
cmp = bytes.Compare(items[i].flightID, items[j].flightID)
|
|
}
|
|
case "updated_at":
|
|
switch {
|
|
case left.UpdatedAt.Before(right.UpdatedAt):
|
|
cmp = -1
|
|
case left.UpdatedAt.After(right.UpdatedAt):
|
|
cmp = 1
|
|
default:
|
|
cmp = bytes.Compare(items[i].flightID, items[j].flightID)
|
|
}
|
|
default:
|
|
switch {
|
|
case left.CreatedAt.Before(right.CreatedAt):
|
|
cmp = -1
|
|
case left.CreatedAt.After(right.CreatedAt):
|
|
cmp = 1
|
|
default:
|
|
cmp = bytes.Compare(items[i].flightID, items[j].flightID)
|
|
}
|
|
}
|
|
if cmp == 0 {
|
|
return false
|
|
}
|
|
if desc {
|
|
return cmp > 0
|
|
}
|
|
return cmp < 0
|
|
})
|
|
}
|
|
|
|
func parseOptionalReserveAcUUIDFilter(raw, sourcePointer string) ([]byte, string, *jsonapi.ErrorObject) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil, "", nil
|
|
}
|
|
id, err := uuidv7.ParseString(raw)
|
|
if err != nil {
|
|
return nil, "", &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: sourcePointer},
|
|
}
|
|
}
|
|
return id, idString(id), nil
|
|
}
|
|
|
|
func reserveAcMatchFilter(row reserveac.ReserveAc, flightID, filter string) bool {
|
|
if filter == "" {
|
|
return true
|
|
}
|
|
haystacks := []string{
|
|
strings.ToLower(strings.TrimSpace(row.Status)),
|
|
strings.ToLower(strings.TrimSpace(idString(row.ID))),
|
|
strings.ToLower(strings.TrimSpace(idString(row.AircraftID))),
|
|
strings.ToLower(strings.TrimSpace(idString(row.InspectionID))),
|
|
strings.ToLower(strings.TrimSpace(flightID)),
|
|
}
|
|
for i := range haystacks {
|
|
if strings.Contains(haystacks[i], filter) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sortReserveAcRows(rows []reserveac.ReserveAc, sortParam string) {
|
|
field := strings.TrimSpace(sortParam)
|
|
desc := false
|
|
if strings.HasPrefix(field, "-") {
|
|
desc = true
|
|
field = strings.TrimPrefix(field, "-")
|
|
}
|
|
switch field {
|
|
case "status", "created_at", "updated_at":
|
|
default:
|
|
field = "created_at"
|
|
desc = true
|
|
}
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
left := rows[i]
|
|
right := rows[j]
|
|
cmp := 0
|
|
switch field {
|
|
case "status":
|
|
ls := strings.ToLower(strings.TrimSpace(left.Status))
|
|
rs := strings.ToLower(strings.TrimSpace(right.Status))
|
|
switch {
|
|
case ls < rs:
|
|
cmp = -1
|
|
case ls > rs:
|
|
cmp = 1
|
|
default:
|
|
switch {
|
|
case left.CreatedAt.Before(right.CreatedAt):
|
|
cmp = -1
|
|
case left.CreatedAt.After(right.CreatedAt):
|
|
cmp = 1
|
|
default:
|
|
cmp = 0
|
|
}
|
|
}
|
|
case "updated_at":
|
|
switch {
|
|
case left.UpdatedAt.Before(right.UpdatedAt):
|
|
cmp = -1
|
|
case left.UpdatedAt.After(right.UpdatedAt):
|
|
cmp = 1
|
|
default:
|
|
cmp = 0
|
|
}
|
|
default:
|
|
switch {
|
|
case left.CreatedAt.Before(right.CreatedAt):
|
|
cmp = -1
|
|
case left.CreatedAt.After(right.CreatedAt):
|
|
cmp = 1
|
|
default:
|
|
cmp = 0
|
|
}
|
|
}
|
|
if cmp == 0 {
|
|
return false
|
|
}
|
|
if desc {
|
|
return cmp > 0
|
|
}
|
|
return cmp < 0
|
|
})
|
|
}
|
|
|
|
type reserveAcResourceOptions struct {
|
|
flightID []byte
|
|
inspection *shareddto.ReserveAcInspectionSummary
|
|
aircraft *shareddto.ReserveAcAircraftSummary
|
|
lastDaily *shareddto.ReserveAcLastDailyInfo
|
|
}
|
|
|
|
func reserveAcResource(row *reserveac.ReserveAc, opts reserveAcResourceOptions) responsedto.ReserveAcResource {
|
|
return responsedto.ReserveAcResource{
|
|
Type: "reserve_ac",
|
|
ID: idString(row.ID),
|
|
Attributes: shareddto.ReserveAcAttributes{
|
|
Status: row.Status,
|
|
AircraftID: idString(row.AircraftID),
|
|
Aircraft: opts.aircraft,
|
|
LastDaily: opts.lastDaily,
|
|
FlightID: idString(opts.flightID),
|
|
Inspection: opts.inspection,
|
|
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
|
|
CreatedBy: uuidStringOrEmpty(row.CreatedBy),
|
|
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
|
|
UpdatedBy: uuidStringOrEmpty(row.UpdatedBy),
|
|
DeletedAt: timeStringOrEmpty(row.DeletedAt),
|
|
DeletedBy: uuidStringOrEmpty(row.DeletedBy),
|
|
},
|
|
}
|
|
}
|
|
|
|
func reserveAcLastDailyInfo(v *shareddto.LastDailyInspection) *shareddto.ReserveAcLastDailyInfo {
|
|
if v == nil {
|
|
return &shareddto.ReserveAcLastDailyInfo{
|
|
Name: "",
|
|
ShortName: "",
|
|
Date: "",
|
|
UTCTime: "",
|
|
}
|
|
}
|
|
t := v.At.UTC()
|
|
return &shareddto.ReserveAcLastDailyInfo{
|
|
Name: strings.TrimSpace(v.Name),
|
|
ShortName: strings.TrimSpace(v.ShortName),
|
|
Date: t.Format("02.01.2006"),
|
|
UTCTime: t.Format("15:04"),
|
|
}
|
|
}
|
|
|
|
func hasReserveAcBeforePayload(v requestdto.ReserveAcBeforeInspectionAttributes) bool {
|
|
if v.OilEngineNR1Checked != nil || v.OilEngineNR2Checked != nil || v.HydraulicLHChecked != nil || v.HydraulicRHChecked != nil {
|
|
return true
|
|
}
|
|
if v.OilTransmissionMGBChecked != nil || v.OilTransmissionIGBChecked != nil || v.OilTransmissionTGBChecked != nil {
|
|
return true
|
|
}
|
|
if v.FuelAmount != nil || v.FuelUnit != nil || v.FuelAddedAmount != nil || v.Note != nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasReserveAcPreparePayload(v requestdto.ReserveAcPrepareInspectionAttributes) bool {
|
|
return v.NotamBriefing != nil || v.WeatherBriefing != nil || v.OperationalFlightPlan != nil
|
|
}
|
|
|
|
func reserveAcUpdateFlightInput(attrs requestdto.ReserveAcUpdateAttributes) (string, string, bool) {
|
|
if attrs.FlightID != nil {
|
|
return strings.TrimSpace(*attrs.FlightID), "/data/attributes/flight_id", true
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func boolPtr(v bool) *bool { return &v }
|
|
|
|
func (h *ReserveAcHandler) buildCreateInspectionSummary(c *fiber.Ctx, inspectionID []byte, helicopterID []byte) (*shareddto.ReserveAcInspectionSummary, error) {
|
|
inspection, err := h.inspect.GetByID(c.UserContext(), inspectionID)
|
|
if err != nil || inspection == nil {
|
|
return nil, err
|
|
}
|
|
ckRows, err := h.fileCk.ListByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isDoneByHeliFileID := make(map[string]bool, len(ckRows))
|
|
for i := range ckRows {
|
|
isDoneByHeliFileID[idString(ckRows[i].HelicopterFileID)] = ckRows[i].IsDone
|
|
}
|
|
|
|
beforeRows, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, helicopterfile.SectionBeforeFirstFlightInspection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prepareRows, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, helicopterfile.SectionFlightPreparationAndWB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
beforeData, _ := h.before.GetByFlightInspectionID(c.UserContext(), inspectionID)
|
|
prepareData, _ := h.prep.GetByFlightInspection(c.UserContext(), inspectionID)
|
|
|
|
beforeSection := h.buildChecklistSectionSummary(c, beforeRows, isDoneByHeliFileID)
|
|
prepareSection := h.buildChecklistSectionSummary(c, prepareRows, isDoneByHeliFileID)
|
|
out := &shareddto.ReserveAcInspectionSummary{
|
|
ID: idString(inspection.ID),
|
|
InspectionDate: inspection.InspectionDate.UTC().Format("2006-01-02"),
|
|
Before: shareddto.ReserveAcBeforeSectionDTO{
|
|
Total: beforeSection.Total,
|
|
Completed: beforeSection.Completed,
|
|
DoneFiles: beforeSection.DoneFiles,
|
|
FileChecklist: beforeSection.FileChecklist,
|
|
},
|
|
Prepare: shareddto.ReserveAcPrepareSectionDTO{
|
|
Total: prepareSection.Total,
|
|
Completed: prepareSection.Completed,
|
|
DoneFiles: prepareSection.DoneFiles,
|
|
FileChecklist: prepareSection.FileChecklist,
|
|
},
|
|
}
|
|
if beforeData != nil {
|
|
out.Before.Fields = &shareddto.ReserveAcBeforeInspectionFields{
|
|
OilEngineNR1Checked: beforeData.OilEngineNR1Checked,
|
|
OilEngineNR2Checked: beforeData.OilEngineNR2Checked,
|
|
HydraulicLHChecked: beforeData.HydraulicLHChecked,
|
|
HydraulicRHChecked: beforeData.HydraulicRHChecked,
|
|
OilTransmissionMGBChecked: beforeData.OilTransmissionMGBChecked,
|
|
OilTransmissionIGBChecked: beforeData.OilTransmissionIGBChecked,
|
|
OilTransmissionTGBChecked: beforeData.OilTransmissionTGBChecked,
|
|
FuelAmount: beforeData.FuelAmount,
|
|
FuelUnit: strings.TrimSpace(beforeData.FuelUnit),
|
|
FuelAddedAmount: beforeData.FuelAddedAmount,
|
|
}
|
|
}
|
|
if prepareData != nil {
|
|
out.Prepare.PrepareFields = &shareddto.ReserveAcPrepareFields{
|
|
NotamBriefing: boolPtr(prepareData.NOTAMBriefing),
|
|
WeatherBriefing: boolPtr(prepareData.WeatherBriefing),
|
|
OperationalFlightPlan: boolPtr(prepareData.OperationalFlightPlan),
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type reserveAcChecklistSectionCommon struct {
|
|
Total int
|
|
Completed int
|
|
DoneFiles []shareddto.ReserveAcFileChecklistItemDTO
|
|
FileChecklist []shareddto.ReserveAcFileChecklistItemDTO
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildChecklistSectionSummary(c *fiber.Ctx, files []helicopterfile.HelicopterFile, isDoneByHeliFileID map[string]bool) reserveAcChecklistSectionCommon {
|
|
out := reserveAcChecklistSectionCommon{
|
|
FileChecklist: make([]shareddto.ReserveAcFileChecklistItemDTO, 0, len(files)),
|
|
DoneFiles: make([]shareddto.ReserveAcFileChecklistItemDTO, 0, len(files)),
|
|
}
|
|
for i := range files {
|
|
if reserveAcChecklistFileCreatedFromTemplate(&files[i]) {
|
|
continue
|
|
}
|
|
fileID := idString(files[i].ID)
|
|
isDone := isDoneByHeliFileID[fileID]
|
|
if isDone {
|
|
out.Completed++
|
|
}
|
|
out.Total++
|
|
fileAttachmentID := ""
|
|
fileName := ""
|
|
if files[i].FileAttachment != nil {
|
|
fileAttachmentID = idString(files[i].FileAttachment.ID)
|
|
if files[i].FileAttachment.File != nil {
|
|
fileName = strings.TrimSpace(files[i].FileAttachment.File.Name)
|
|
}
|
|
}
|
|
downloadURL := ""
|
|
if files[i].FileAttachment != nil && files[i].FileAttachment.File != nil {
|
|
downloadURL = resolveFileManagerDownloadURL(c.UserContext(), h.storage, files[i].FileAttachment.File)
|
|
}
|
|
item := shareddto.ReserveAcFileChecklistItemDTO{
|
|
HelicopterFileID: fileID,
|
|
FileAttachmentID: fileAttachmentID,
|
|
FileName: fileName,
|
|
DownloadURL: downloadURL,
|
|
IsDone: isDone,
|
|
}
|
|
out.FileChecklist = append(out.FileChecklist, item)
|
|
if item.IsDone {
|
|
out.DoneFiles = append(out.DoneFiles, item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// reserveAcChecklistFileCreatedFromTemplate reports whether a helicopter file row is a
|
|
// document instantiated from a template. Those rows should stay hidden from the
|
|
// checklist summary so only catalog template entries appear in the UI.
|
|
func reserveAcChecklistFileCreatedFromTemplate(row *helicopterfile.HelicopterFile) bool {
|
|
if row == nil {
|
|
return false
|
|
}
|
|
if row.SourceFile != nil {
|
|
return len(row.SourceFile.TemplateUUID) == 16
|
|
}
|
|
if row.FileAttachment != nil && row.FileAttachment.File != nil {
|
|
return len(row.FileAttachment.File.TemplateUUID) == 16
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildInspectionChecklistSection(c *fiber.Ctx, inspectionID []byte, helicopterID []byte, section string) (reserveAcChecklistSectionCommon, error) {
|
|
ckRows, err := h.fileCk.ListByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return reserveAcChecklistSectionCommon{}, err
|
|
}
|
|
isDoneByHeliFileID := make(map[string]bool, len(ckRows))
|
|
for i := range ckRows {
|
|
isDoneByHeliFileID[idString(ckRows[i].HelicopterFileID)] = ckRows[i].IsDone
|
|
}
|
|
|
|
templateFiles, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, section)
|
|
if err != nil {
|
|
return reserveAcChecklistSectionCommon{}, err
|
|
}
|
|
return h.buildChecklistSectionSummary(c, templateFiles, isDoneByHeliFileID), nil
|
|
}
|
|
|
|
type afterFlightInspectionSummary struct {
|
|
MissionCode string
|
|
MissionType []string
|
|
TotalFlight int
|
|
FlightDataCount int
|
|
FlightTimeSeconds int64
|
|
Route []string
|
|
}
|
|
|
|
func afterFlightInspectionPayload(
|
|
inspectionID []byte,
|
|
inspectionRow *flightinspection.FlightInspection,
|
|
helicopterRow *responsedto.HelicopterResource,
|
|
row *afterflightinspection.AfterFlightInspection,
|
|
section reserveAcChecklistSectionCommon,
|
|
summary *afterFlightInspectionSummary,
|
|
) rootdto.AfterFlightInspectionResponse {
|
|
inspectionIDStr := idString(inspectionID)
|
|
payload := rootdto.AfterFlightInspectionResponse{
|
|
Data: rootdto.AfterFlightInspectionResource{
|
|
Type: "after_flight_inspection",
|
|
Attributes: rootdto.AfterFlightInspectionAttributes{
|
|
FlightInspectionID: inspectionIDStr,
|
|
Total: section.Total,
|
|
Completed: section.Completed,
|
|
DoneFileChecklist: toAfterFlightChecklistItems(section.DoneFiles),
|
|
FileChecklist: toAfterFlightChecklistItems(section.FileChecklist),
|
|
Helicopter: helicopterRow,
|
|
MissionType: []string{},
|
|
Route: []string{},
|
|
},
|
|
},
|
|
}
|
|
if inspectionRow != nil {
|
|
payload.Data.Attributes.FlightInspection = afterFlightInspectionResource(inspectionRow)
|
|
}
|
|
if summary != nil {
|
|
payload.Data.Attributes.MissionCode = summary.MissionCode
|
|
payload.Data.Attributes.MissionType = append([]string(nil), summary.MissionType...)
|
|
payload.Data.Attributes.TotalFlight = summary.TotalFlight
|
|
payload.Data.Attributes.FlightDataCount = summary.FlightDataCount
|
|
payload.Data.Attributes.FlightTimeSeconds = summary.FlightTimeSeconds
|
|
payload.Data.Attributes.Route = append([]string(nil), summary.Route...)
|
|
}
|
|
if row == nil {
|
|
return payload
|
|
}
|
|
|
|
rowID := idString(row.ID)
|
|
payload.Data.ID = rowID
|
|
payload.Data.Attributes.ID = rowID
|
|
payload.Data.Attributes.OilEngineNR1Checked = row.OilEngineNR1Checked
|
|
payload.Data.Attributes.OilEngineNR2Checked = row.OilEngineNR2Checked
|
|
payload.Data.Attributes.HydraulicLHChecked = row.HydraulicLHChecked
|
|
payload.Data.Attributes.HydraulicRHChecked = row.HydraulicRHChecked
|
|
payload.Data.Attributes.OilTransmissionMGBChecked = row.OilTransmissionMGBChecked
|
|
payload.Data.Attributes.OilTransmissionIGBChecked = row.OilTransmissionIGBChecked
|
|
payload.Data.Attributes.OilTransmissionTGBChecked = row.OilTransmissionTGBChecked
|
|
payload.Data.Attributes.Note = row.Note
|
|
payload.Data.Attributes.CreatedAt = row.CreatedAt.UTC().Format(time.RFC3339)
|
|
payload.Data.Attributes.UpdatedAt = row.UpdatedAt.UTC().Format(time.RFC3339)
|
|
|
|
return payload
|
|
}
|
|
|
|
func (h *ReserveAcHandler) afterFlightInspectionHelicopterResource(ctx context.Context, reserveAcRow *reserveac.ReserveAc) *responsedto.HelicopterResource {
|
|
if h == nil || reserveAcRow == nil || len(reserveAcRow.AircraftID) != 16 {
|
|
return nil
|
|
}
|
|
|
|
helicopterRow := reserveAcRow.Aircraft
|
|
if h.aircraft != nil {
|
|
if fullRow, err := h.aircraft.GetByID(ctx, reserveAcRow.AircraftID); err == nil && fullRow != nil {
|
|
helicopterRow = fullRow
|
|
}
|
|
}
|
|
if helicopterRow == nil {
|
|
return nil
|
|
}
|
|
|
|
isInUse := false
|
|
if h.aircraft != nil {
|
|
if inUse, err := h.aircraft.IsInUse(ctx, reserveAcRow.AircraftID); err == nil {
|
|
isInUse = inUse
|
|
}
|
|
}
|
|
|
|
var reserveAcProvider helicopterLastDailyInspectionProvider
|
|
if provider, ok := h.svc.(helicopterLastDailyInspectionProvider); ok {
|
|
reserveAcProvider = provider
|
|
}
|
|
res := helicopterResourceWithStorage(ctx, h.storage, reserveAcProvider, helicopterRow, isInUse)
|
|
return &res
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildAfterFlightInspectionSummary(ctx context.Context, reserveAcRow *reserveac.ReserveAc) *afterFlightInspectionSummary {
|
|
summary := &afterFlightInspectionSummary{}
|
|
if h == nil || h.flight == nil || reserveAcRow == nil || len(reserveAcRow.ID) != 16 {
|
|
return summary
|
|
}
|
|
|
|
flights, err := h.flight.ListByReserveAcID(ctx, reserveAcRow.ID)
|
|
if err != nil || len(flights) == 0 {
|
|
return summary
|
|
}
|
|
|
|
if h.mission == nil {
|
|
return summary
|
|
}
|
|
|
|
missionTypeSeen := make(map[string]struct{})
|
|
route := make([]string, 0)
|
|
for f := range flights {
|
|
if len(flights[f].ID) != 16 {
|
|
continue
|
|
}
|
|
if summary.MissionCode == "" {
|
|
summary.MissionCode = strings.TrimSpace(stringPtrOrEmpty(flights[f].MissionCode))
|
|
}
|
|
|
|
missions, missErr := h.mission.ListByFlightID(ctx, flights[f].ID)
|
|
if missErr != nil {
|
|
continue
|
|
}
|
|
|
|
for i := range missions {
|
|
missionType := strings.ToUpper(strings.TrimSpace(missions[i].Type))
|
|
if missionType != "" {
|
|
if _, ok := missionTypeSeen[missionType]; !ok {
|
|
missionTypeSeen[missionType] = struct{}{}
|
|
summary.MissionType = append(summary.MissionType, missionType)
|
|
}
|
|
}
|
|
if h.flightData == nil || len(missions[i].ID) != 16 {
|
|
continue
|
|
}
|
|
fds, fdErr := h.flightData.ListByMissionID(ctx, missions[i].ID)
|
|
if fdErr != nil {
|
|
continue
|
|
}
|
|
summary.TotalFlight += len(fds)
|
|
for j := range fds {
|
|
summary.FlightDataCount++
|
|
summary.FlightTimeSeconds += flightDataFlightTimeSeconds(&fds[j])
|
|
|
|
fromLabel := flightDataLocationLabel(fromLocationRefFlightData(&fds[j]))
|
|
if fromLabel != "" {
|
|
route = appendRoutePoint(route, fromLabel)
|
|
}
|
|
toLabel := flightDataLocationLabel(toLocationRefFlightData(&fds[j]))
|
|
if toLabel != "" {
|
|
route = appendRoutePoint(route, toLabel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
summary.Route = route
|
|
return summary
|
|
}
|
|
|
|
func (h *ReserveAcHandler) loadMissionFlightData(ctx context.Context, row mission.Mission) (*flightdata.FlightData, bool) {
|
|
hasFlightData := len(row.FlightDataID) == 16
|
|
if h == nil || h.flightData == nil {
|
|
return nil, hasFlightData
|
|
}
|
|
|
|
if len(row.ID) == 16 {
|
|
if fd, err := h.flightData.GetByMissionID(ctx, row.ID); err == nil && fd != nil {
|
|
return fd, true
|
|
}
|
|
}
|
|
if len(row.FlightDataID) == 16 {
|
|
if fd, err := h.flightData.GetByID(ctx, row.FlightDataID); err == nil && fd != nil {
|
|
return fd, true
|
|
}
|
|
}
|
|
return nil, hasFlightData
|
|
}
|
|
|
|
func appendRoutePoint(route []string, point string) []string {
|
|
point = strings.TrimSpace(point)
|
|
if point == "" {
|
|
return route
|
|
}
|
|
if len(route) == 0 || route[len(route)-1] != point {
|
|
return append(route, point)
|
|
}
|
|
return route
|
|
}
|
|
|
|
func flightDataLocationLabel(ref *responsedto.FlightDataLocationRef) string {
|
|
if ref == nil {
|
|
return ""
|
|
}
|
|
for _, candidate := range []string{ref.Label, ref.Code, ref.Name, ref.ID} {
|
|
if trimmed := strings.TrimSpace(candidate); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func flightDataFlightTimeSeconds(row *flightdata.FlightData) int64 {
|
|
if row == nil {
|
|
return 0
|
|
}
|
|
if !row.FlightTakeOff.IsZero() && !row.FlightLanding.IsZero() && row.FlightLanding.After(row.FlightTakeOff) {
|
|
return int64(row.FlightLanding.Sub(row.FlightTakeOff).Seconds())
|
|
}
|
|
if row.FlightDuration > 0 {
|
|
return int64(row.FlightDuration.Seconds())
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func toAfterFlightChecklistItems(items []shareddto.ReserveAcFileChecklistItemDTO) []rootdto.AfterFlightInspectionFileChecklistItem {
|
|
if len(items) == 0 {
|
|
return []rootdto.AfterFlightInspectionFileChecklistItem{}
|
|
}
|
|
out := make([]rootdto.AfterFlightInspectionFileChecklistItem, 0, len(items))
|
|
for i := range items {
|
|
out = append(out, rootdto.AfterFlightInspectionFileChecklistItem{
|
|
HelicopterFileID: items[i].HelicopterFileID,
|
|
FileName: items[i].FileName,
|
|
DownloadURL: items[i].DownloadURL,
|
|
IsDone: items[i].IsDone,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (h *ReserveAcHandler) calculateReserveAcStatus(summary *shareddto.ReserveAcInspectionSummary) string {
|
|
if summary == nil {
|
|
return reserveac.StatusPending
|
|
}
|
|
|
|
totalChecklist := summary.Before.Total + summary.Prepare.Total
|
|
doneChecklist := summary.Before.Completed + summary.Prepare.Completed
|
|
|
|
if totalChecklist > 0 {
|
|
if doneChecklist == 0 {
|
|
return reserveac.StatusPending
|
|
}
|
|
if doneChecklist < totalChecklist {
|
|
return reserveac.StatusOnProgress
|
|
}
|
|
return reserveac.StatusApproved
|
|
}
|
|
|
|
// Fallback when no checklist templates exist: use inspection field presence as progress signal.
|
|
if summary.Before.Fields != nil || summary.Prepare.PrepareFields != nil {
|
|
return reserveac.StatusOnProgress
|
|
}
|
|
|
|
return reserveac.StatusPending
|
|
}
|
|
|
|
func (h *ReserveAcHandler) buildAircraftSummary(c *fiber.Ctx, aircraftID []byte) (*shareddto.ReserveAcAircraftSummary, error) {
|
|
if h.aircraft == nil || len(aircraftID) != 16 {
|
|
return nil, nil
|
|
}
|
|
row, err := h.aircraft.GetByID(c.UserContext(), aircraftID)
|
|
if err != nil || row == nil {
|
|
return nil, err
|
|
}
|
|
return &shareddto.ReserveAcAircraftSummary{
|
|
ID: idString(row.ID),
|
|
Designation: strings.TrimSpace(row.Designation),
|
|
Identifier: strings.TrimSpace(row.Identifier),
|
|
Type: strings.TrimSpace(row.Type),
|
|
AOG: row.AirOnGround,
|
|
IsActive: row.IsActive,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ReserveAcHandler) upsertInspectionFileChecklistSection(
|
|
c *fiber.Ctx,
|
|
inspectionID []byte,
|
|
helicopterID []byte,
|
|
section string,
|
|
payload []requestdto.ReserveAcFileChecklistItem,
|
|
payloadPointer string,
|
|
reserveAcID []byte,
|
|
) (bool, error) {
|
|
templateFiles, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, section)
|
|
if err != nil {
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
type requestedChecklist struct {
|
|
fileID []byte
|
|
isDone bool
|
|
}
|
|
requested := make([]requestedChecklist, 0, len(payload))
|
|
for i := range payload {
|
|
fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(payload[i].HelicopterFileID))
|
|
if parseErr != nil {
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_file_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
requested = append(requested, requestedChecklist{
|
|
fileID: fileID,
|
|
isDone: payload[i].IsDone,
|
|
})
|
|
}
|
|
requestedByFileID := make(map[string]bool, len(requested))
|
|
for i := range requested {
|
|
requestedByFileID[string(requested[i].fileID)] = requested[i].isDone
|
|
}
|
|
|
|
existingRows, err := h.fileCk.ListByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
existingByFileID := make(map[string]*flightinspectionfilechecklist.FlightInspectionFileChecklist, len(existingRows))
|
|
for i := range existingRows {
|
|
fileID := string(existingRows[i].HelicopterFileID)
|
|
existingByFileID[fileID] = &existingRows[i]
|
|
}
|
|
|
|
templateSet := make(map[string]struct{}, len(templateFiles))
|
|
for i := range templateFiles {
|
|
fileKey := string(templateFiles[i].ID)
|
|
templateSet[fileKey] = struct{}{}
|
|
|
|
current := existingByFileID[fileKey]
|
|
targetDone, hasOverride := requestedByFileID[fileKey]
|
|
|
|
if templateFiles[i].IsMandatory && !targetDone {
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "file_checklist must be fully checked",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
if current == nil {
|
|
// New row defaults to false unless explicitly provided in payload.
|
|
if !hasOverride {
|
|
targetDone = false
|
|
}
|
|
newRow := &flightinspectionfilechecklist.FlightInspectionFileChecklist{
|
|
FlightInspectionID: inspectionID,
|
|
HelicopterFileID: templateFiles[i].ID,
|
|
IsDone: targetDone,
|
|
CreatedBy: actorUserID(c),
|
|
UpdatedBy: actorUserID(c),
|
|
}
|
|
if err := h.fileCk.Create(c.UserContext(), newRow); err != nil {
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Partial update behavior: keep existing value if file is omitted.
|
|
if !hasOverride {
|
|
targetDone = current.IsDone
|
|
}
|
|
|
|
if current.IsDone != targetDone {
|
|
current.IsDone = targetDone
|
|
current.UpdatedBy = actorUserID(c)
|
|
if err := h.fileCk.Update(c.UserContext(), current); err != nil {
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: "an internal error occurred",
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := range requested {
|
|
if _, ok := templateSet[string(requested[i].fileID)]; ok {
|
|
continue
|
|
}
|
|
if len(reserveAcID) == 16 {
|
|
_ = h.svc.Delete(c.UserContext(), reserveAcID, actorUserID(c))
|
|
}
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_file_id does not belong to selected helicopter/section",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (h *ReserveAcHandler) validateInspectionChecklistSectionComplete(
|
|
c *fiber.Ctx,
|
|
helicopterID []byte,
|
|
section string,
|
|
payload []requestdto.ReserveAcFileChecklistItem,
|
|
payloadPointer string,
|
|
) (bool, error) {
|
|
if h == nil || h.heliFile == nil {
|
|
if err := writeReserveAcErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Validation failed",
|
|
Detail: "helicopter file service is not configured",
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
templateFiles, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, section)
|
|
if err != nil {
|
|
if err := writeReserveAcErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Validation failed",
|
|
Detail: "an internal error occurred",
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
templateSet := make(map[string]struct{}, len(templateFiles))
|
|
for i := range templateFiles {
|
|
templateSet[string(templateFiles[i].ID)] = struct{}{}
|
|
}
|
|
payloadByFileID := make(map[string]bool, len(payload))
|
|
seen := make(map[string]struct{}, len(payload))
|
|
for i := range payload {
|
|
fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(payload[i].HelicopterFileID))
|
|
if parseErr != nil {
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_file_id is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
key := string(fileID)
|
|
if _, ok := templateSet[key]; !ok {
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_file_id does not belong to selected helicopter/section",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "helicopter_file_id is duplicated",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
seen[key] = struct{}{}
|
|
payloadByFileID[key] = payload[i].IsDone
|
|
}
|
|
|
|
for i := range templateFiles {
|
|
fileKey := string(templateFiles[i].ID)
|
|
done, ok := payloadByFileID[fileKey]
|
|
if !ok {
|
|
done = false
|
|
}
|
|
if templateFiles[i].IsMandatory && !done {
|
|
if err := writeReserveAcErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "file_checklist must be fully checked",
|
|
Source: &jsonapi.ErrorSource{Pointer: payloadPointer},
|
|
}}); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (h *ReserveAcHandler) deleteInspectionFileChecklistSection(
|
|
c *fiber.Ctx,
|
|
inspectionID []byte,
|
|
helicopterID []byte,
|
|
section string,
|
|
) error {
|
|
templateFiles, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, section)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
templateSet := make(map[string]struct{}, len(templateFiles))
|
|
for i := range templateFiles {
|
|
templateSet[string(templateFiles[i].ID)] = struct{}{}
|
|
}
|
|
|
|
existingRows, err := h.fileCk.ListByInspectionID(c.UserContext(), inspectionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range existingRows {
|
|
if _, ok := templateSet[string(existingRows[i].HelicopterFileID)]; !ok {
|
|
continue
|
|
}
|
|
if err := h.fileCk.Delete(c.UserContext(), existingRows[i].ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|