Files
fm_be/internal/transport/http/handlers/reserve_ac_handler.go
2026-07-16 22:16:45 +07:00

3136 lines
103 KiB
Go

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