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 }