package mysql import ( "context" "errors" "time" "gorm.io/gorm" "wucher/internal/domain/flight" ) type FlightRepository struct { db *gorm.DB } func preloadTakeoverRelations(q *gorm.DB) *gorm.DB { return q. Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.Base.OperationalShiftTimes"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople") } func NewFlightRepository(db *gorm.DB) *FlightRepository { return &FlightRepository{db: db} } func (r *FlightRepository) Create(ctx context.Context, row *flight.Flight) error { return r.db.WithContext(ctx).Create(row).Error } func (r *FlightRepository) GetLatestMissionCodeByPrefix(ctx context.Context, prefix string) (string, error) { var out struct { MissionCode string `gorm:"column:mission_code"` } err := r.db.WithContext(ctx). Table("flights"). Select("mission_code"). Where("mission_code LIKE ? AND deleted_at IS NULL", prefix+"%"). Order("mission_code DESC"). Limit(1). Scan(&out).Error if err != nil { return "", err } return out.MissionCode, nil } func (r *FlightRepository) Update(ctx context.Context, row *flight.Flight) error { return r.db.WithContext(ctx).Save(row).Error } func (r *FlightRepository) Delete(ctx context.Context, id []byte, deletedBy []byte) error { now := time.Now().UTC() updates := map[string]any{ "deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy, } return r.db.WithContext(ctx). Model(&flight.Flight{}). Where("id = ? AND deleted_at IS NULL", id). Updates(updates).Error } func (r *FlightRepository) GetByID(ctx context.Context, id []byte) (*flight.Flight, error) { var row flight.Flight err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.Base.OperationalShiftTimes"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Where("flights.id = ? AND flights.deleted_at IS NULL", id). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightRepository) ListByUserID(ctx context.Context, userID []byte, limit, offset int) ([]flight.Flight, int64, error) { rows := make([]flight.Flight, 0) var total int64 base := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Where("flights.deleted_at IS NULL"). Where( `EXISTS ( SELECT 1 FROM duty_roster_crews c JOIN duty_rosters r ON r.id = c.roster_id WHERE c.user_id = ? AND c.deleted_at IS NULL AND r.deleted_at IS NULL AND r.flight_id = flights.id )`, userID, ) if err := base.Count(&total).Error; err != nil { return nil, 0, err } query := base.Order("flights.date DESC, flights.created_at DESC") if limit > 0 { query = query.Limit(limit).Offset(offset) } if err := query.Find(&rows).Error; err != nil { return nil, 0, err } return rows, total, nil } func (r *FlightRepository) ListByCreatedBy(ctx context.Context, createdBy []byte, filter string, date string, sort string, limit, offset int) ([]flight.Flight, int64, error) { rows := make([]flight.Flight, 0) var total int64 base := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Where("flights.deleted_at IS NULL"). Where("flights.created_by = ?", createdBy) if filter != "" { like := "%" + filter + "%" base = base.Where("flights.mission_code LIKE ?", like) } if date != "" { base = base.Where("flights.date = ?", date) } if err := base.Count(&total).Error; err != nil { return nil, 0, err } query := base if sort != "" { query = query.Order(sort) } else { query = query.Order("flights.date DESC, flights.created_at DESC") } if limit > 0 { query = query.Limit(limit).Offset(offset) } if err := query.Find(&rows).Error; err != nil { return nil, 0, err } return rows, total, nil } func (r *FlightRepository) GetByReserveAcID(ctx context.Context, reserveAcID []byte) (*flight.Flight, error) { var row flight.Flight err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Joins("JOIN takeover_acs ta ON ta.id = flights.takeover_ac_id AND ta.deleted_at IS NULL"). Where("ta.reserve_ac_id = ? AND flights.deleted_at IS NULL", reserveAcID). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightRepository) ListByReserveAcID(ctx context.Context, reserveAcID []byte) ([]flight.Flight, error) { rows := make([]flight.Flight, 0) err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Joins("JOIN takeover_acs ta ON ta.id = flights.takeover_ac_id AND ta.deleted_at IS NULL"). Where("ta.reserve_ac_id = ? AND flights.deleted_at IS NULL", reserveAcID). Order("flights.created_at ASC, flights.id ASC"). Find(&rows).Error if err != nil { return nil, err } return rows, nil } func (r *FlightRepository) GetByTakeoverAcID(ctx context.Context, takeoverAcID []byte) (*flight.Flight, error) { var row flight.Flight err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Where("flights.takeover_ac_id = ? AND flights.deleted_at IS NULL", takeoverAcID). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightRepository) ListByTakeoverAcIDs(ctx context.Context, takeoverAcIDs [][]byte) ([]flight.Flight, error) { if len(takeoverAcIDs) == 0 { return []flight.Flight{}, nil } rows := make([]flight.Flight, 0) err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightListSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Where("flights.takeover_ac_id IN ? AND flights.deleted_at IS NULL", takeoverAcIDs). Find(&rows).Error if err != nil { return nil, err } return rows, nil } func (r *FlightRepository) GetByDutyRosterID(ctx context.Context, dutyRosterID []byte) (*flight.Flight, error) { var row flight.Flight err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightListSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("JOIN duty_rosters dr ON dr.flight_id = flights.id AND dr.deleted_at IS NULL"). Where("dr.id = ? AND flights.deleted_at IS NULL", dutyRosterID). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightRepository) ListByDutyRosterIDs(ctx context.Context, dutyRosterIDs [][]byte) ([]flight.Flight, error) { if len(dutyRosterIDs) == 0 { return []flight.Flight{}, nil } rows := make([]flight.Flight, 0) err := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightListSelectSQL()). Preload("Takeover"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Joins("JOIN duty_rosters dr ON dr.flight_id = flights.id AND dr.deleted_at IS NULL"). Where("dr.id IN ? AND flights.deleted_at IS NULL", dutyRosterIDs). Find(&rows).Error if err != nil { return nil, err } return rows, nil } func (r *FlightRepository) List(ctx context.Context, filter string, date string, sort string, limit, offset int) ([]flight.Flight, int64, error) { rows := make([]flight.Flight, 0) var total int64 base := r.db.WithContext(ctx). Model(&flight.Flight{}). Select(flightReadSelectSQL()). Preload("Takeover"). Preload("Takeover.Base"). Preload("Takeover.ReserveAc"). Preload("Takeover.ReserveAc.Aircraft"). Preload("Takeover.ReserveAc.Inspection"). Preload("Takeover.RosterCrews"). Preload("Takeover.OtherPeople"). Joins("LEFT JOIN users cu ON cu.id = flights.created_by"). Joins("LEFT JOIN users uu ON uu.id = flights.updated_by"). Where("flights.deleted_at IS NULL") if filter != "" { like := "%" + filter + "%" base = base.Where("flights.mission_code LIKE ?", like) } if date != "" { base = base.Where("flights.date = ?", date) } query := base if sort != "" { query = query.Order(sort) } if err := base.Count(&total).Error; err != nil { return nil, 0, err } if limit > 0 { query = query.Limit(limit).Offset(offset) } if err := query.Find(&rows).Error; err != nil { return nil, 0, err } return rows, total, nil } func flightListSelectSQL() string { // created_by_name / updated_by_name are resolved via correlated subqueries so the list // queries carry the auditor display names without needing extra user JOINs (the read // query joins users; the list variants don't). Keeps get-all in parity with get-by-id. return `flights.*, (SELECT dr.id FROM duty_rosters dr WHERE dr.flight_id = flights.id AND dr.deleted_at IS NULL LIMIT 1) AS duty_roster_id, (SELECT ta.reserve_ac_id FROM takeover_acs ta WHERE ta.id = flights.takeover_ac_id AND ta.deleted_at IS NULL LIMIT 1) AS reserve_ac_id, COALESCE((SELECT NULLIF(TRIM(CONCAT(COALESCE(cu.first_name, ''), ' ', COALESCE(cu.last_name, ''))), '') FROM users cu WHERE cu.id = flights.created_by), '') AS created_by_name, COALESCE((SELECT NULLIF(TRIM(CONCAT(COALESCE(uu.first_name, ''), ' ', COALESCE(uu.last_name, ''))), '') FROM users uu WHERE uu.id = flights.updated_by), '') AS updated_by_name` } func flightReadSelectSQL() string { return `flights.*, (SELECT dr.id FROM duty_rosters dr WHERE dr.flight_id = flights.id AND dr.deleted_at IS NULL LIMIT 1) AS duty_roster_id, (SELECT ta.reserve_ac_id FROM takeover_acs ta WHERE ta.id = flights.takeover_ac_id AND ta.deleted_at IS NULL LIMIT 1) AS reserve_ac_id, COALESCE( NULLIF(TRIM(CONCAT(COALESCE(cu.first_name, ''), ' ', COALESCE(cu.last_name, ''))), ''), '' ) AS created_by_name, COALESCE( NULLIF(TRIM(CONCAT(COALESCE(uu.first_name, ''), ' ', COALESCE(uu.last_name, ''))), ''), '' ) AS updated_by_name` }