diff --git a/cmd/database/migration.go b/cmd/database/migration.go index 8b36991..cf88a10 100644 --- a/cmd/database/migration.go +++ b/cmd/database/migration.go @@ -200,7 +200,7 @@ func MigrateUsers(oldDbConn *gorm.DB) { Home: true, Visiting: false, Status: "Active", - DeletedAt: nil, + DeletedAt: time.Time{}, } if err := roster.Create(); err != nil { @@ -220,7 +220,7 @@ func MigrateUsers(oldDbConn *gorm.DB) { Home: false, Visiting: true, Status: "Active", - DeletedAt: nil, + DeletedAt: time.Time{}, } if err := roster.Create(); err != nil { diff --git a/go.mod b/go.mod index efb0068..0635d58 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/swaggo/swag v1.16.3 go.uber.org/zap v1.27.0 golang.org/x/oauth2 v0.23.0 + gorm.io/datatypes v1.2.5 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 ) @@ -45,6 +46,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,7 +57,7 @@ require ( golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 28ca650..5cf1f40 100644 --- a/go.sum +++ b/go.sum @@ -58,12 +58,26 @@ github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaC github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -82,6 +96,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -122,8 +140,8 @@ golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -140,8 +158,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -154,8 +172,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/internal/training/ots.go b/internal/training/ots.go new file mode 100644 index 0000000..47068ec --- /dev/null +++ b/internal/training/ots.go @@ -0,0 +1,91 @@ +package training + +type Rating string + +const ( + Empty Rating = "" + Satisfactory Rating = "Satisfactory" + Unsatisfactory Rating = "Unsatisfactory" +) + +// KPI - Key Performance Indicator +type KPI struct { + Order uint `json:"order"` + Title string `json:"title"` + Description string `json:"description"` + Required bool `json:"required"` + Rating `json:"rating"` +} + +// KPA - Key Performance Area +type KPA struct { + Order uint `json:"order"` + Title string `json:"title"` + Description string `json:"description"` + Indicators []KPI `json:"indicators"` +} + +// KPC - Key performance Category +type KPC struct { + Order uint `json:"order"` + Title string `json:"title"` + Areas []KPA `json:"areas"` +} +type OTS struct { + Title string `json:"title"` + Categories []KPC `json:"categories"` +} + +var S2OTS = OTS{ + Title: "S2 (Tower) Rating Form", + Categories: []KPC{ + { + Order: 1, + Title: "Theory", + Areas: []KPA{ + { + Order: 1, + Title: "Clearance Delivery + Ground Control", + Description: "Demonstrates knowledge of Delivery and Ground Controller duties and responsibilities", + Indicators: []KPI{ + { + Order: 1, + Title: "Defines all parts of a clearance", + Description: "...", + Required: true, + Rating: Empty, + }, + { + Order: 2, + Title: "Explains all types of SIDs", + Description: "...", + Required: true, + Rating: Empty, + }, + }, + }, + { + Order: 2, + Title: "Local Control", + Description: "Demonstrates knowledge of Local Controller duties and responsibilities", + Indicators: []KPI{ + { + Order: 1, + Title: "Identifies difference between movement and non-movement areas", + Description: "...", + Required: true, + Rating: Empty, + }, + { + Order: 2, + Title: "Defines all parts of VFR traffic pattern", + Description: "...", + Required: true, + Rating: Empty, + }, + }, + }, + }, + }, + }, +} diff --git a/pkg/constants/role.go b/pkg/constants/role.go index 023b385..0432516 100644 --- a/pkg/constants/role.go +++ b/pkg/constants/role.go @@ -435,6 +435,26 @@ func (r RoleID) IsValidRole() bool { return ok } +func (r RoleID) IsFacilityStaff() bool { + switch r { + case AirTrafficManagerRole, DeputyAirTrafficManagerRole, TrainingAdministratorRole: + return true + case EventCoordinatorRole, FacilityEngineerRole, WebMasterRole: + return true + } + + return false +} + +func (r RoleID) IsAssistant() bool { + switch r { + case AssistantEventCoordinator, AssistantFacilityEngineer, AssistantWebMasterRole: + return true + } + + return false +} + func (r RoleID) DisplayName() string { return Roles[r].Name } diff --git a/pkg/database/models/rating_change.go b/pkg/database/models/rating_change.go index 0bd832b..5d8c936 100644 --- a/pkg/database/models/rating_change.go +++ b/pkg/database/models/rating_change.go @@ -37,7 +37,16 @@ func GetAllRatingChanges() ([]RatingChange, error) { return ratingChanges, database.DB.Find(&ratingChanges).Error } -func GetAllRatingChangesByCID(cid uint) ([]RatingChange, error) { +func GetFilteredRatingChanges(cid uint, dateAfter time.Time) ([]RatingChange, error) { var ratingChanges []RatingChange - return ratingChanges, database.DB.Where("cid = ?", cid).Find(&ratingChanges).Error + + query := database.DB + if cid != 0 { + query = query.Where("cid = ?", cid) + } + if !dateAfter.IsZero() { + query = query.Where("created_at > ?", dateAfter) + } + + return ratingChanges, query.Find(&ratingChanges).Error } diff --git a/pkg/database/models/roster.go b/pkg/database/models/roster.go index d311cdb..6fa4174 100644 --- a/pkg/database/models/roster.go +++ b/pkg/database/models/roster.go @@ -19,7 +19,7 @@ type Roster struct { Roles []UserRole `json:"roles" gorm:"foreignKey:RosterID"` CreatedAt time.Time `json:"created_at" example:"2021-01-01T00:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` - DeletedAt gorm.DeletedAt `json:"deleted_at" example:"2021-01-01T00:00:00Z"` // Soft Deletes for logging + DeletedAt time.Time `json:"deleted_at" example:"2021-01-01T00:00:00Z"` } func (r *Roster) BeforeCreate(tx *gorm.DB) error { diff --git a/pkg/database/models/roster_request.go b/pkg/database/models/roster_request.go index b40a023..7dff689 100644 --- a/pkg/database/models/roster_request.go +++ b/pkg/database/models/roster_request.go @@ -4,6 +4,7 @@ import ( "github.com/VATUSA/primary-api/pkg/constants" "github.com/VATUSA/primary-api/pkg/database" "github.com/VATUSA/primary-api/pkg/database/types" + "gorm.io/gorm" "time" ) @@ -18,12 +19,77 @@ type RosterRequest struct { UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` } +func (rr *RosterRequest) BeforeUpdate(tx *gorm.DB) error { + oldRR := &RosterRequest{ID: rr.ID} + if err := oldRR.Get(); err != nil { + return err + } + if oldRR.Status == types.Pending && rr.Status == types.Accepted { + roster := &Roster{ + CID: rr.CID, + Facility: rr.Facility, + OIs: "", + Home: false, + Visiting: false, + Status: "Active", + } + + if rr.RequestType == types.Visiting { + roster.Visiting = true + } else { + roster.Home = true + } + + if err := roster.Create(); err != nil { + return err + } + + // Transfers: + // On accepting a transfer remove the user from their current facility + if rr.RequestType == types.Transferring { + rosters, err := GetRostersByCID(rr.CID) + if err != nil { + return err + } + + for _, r := range rosters { + if r.Facility != rr.Facility && r.Home { + // if the user is an assistant add them to the new facility as a visitor + for _, role := range r.Roles { + if role.RoleID.IsAssistant() { + roster := &Roster{ + CID: r.CID, + Facility: r.Facility, + OIs: r.OIs, + Home: false, + Visiting: true, + Status: "Active", + } + + if err := roster.Create(); err != nil { + return err + } + break + } + } + + if err := r.Delete(); err != nil { + return err + } + } + } + } + } + + return nil +} + func (rr *RosterRequest) Create() error { return database.DB.Create(rr).Error } func (rr *RosterRequest) Update() error { - return database.DB.Save(rr).Error + return database.DB.Updates(rr).Error } func (rr *RosterRequest) Delete() error { @@ -39,6 +105,23 @@ func GetAllRosterRequests() ([]RosterRequest, error) { return rosterRequests, database.DB.Find(&rosterRequests).Error } +func GetFilteredRosterRequests(cid uint, reqType types.RequestType, dateAfter time.Time) ([]RosterRequest, error) { + var rosterRequests []RosterRequest + + query := database.DB + if cid != 0 { + query = query.Where("cid = ?", cid) + } + if reqType != "" { + query = query.Where("request_type = ?", reqType) + } + if !dateAfter.IsZero() { + query = query.Where("created_at > ?", dateAfter) + } + + return rosterRequests, query.Find(&rosterRequests).Error +} + func GetAllRosterRequestsByCID(cid uint) ([]RosterRequest, error) { var rosterRequests []RosterRequest return rosterRequests, database.DB.Where("cid = ?", cid).Find(&rosterRequests).Error diff --git a/pkg/database/models/setup.go b/pkg/database/models/setup.go index d14db72..678a71a 100644 --- a/pkg/database/models/setup.go +++ b/pkg/database/models/setup.go @@ -28,6 +28,9 @@ func AutoMigrate() { &UserNotification{}, &UserFlag{}, &UserRole{}, + &OTSTemplate{}, + &RatingExamRecord{}, + &TrainingNotes{}, ) if err != nil { log.Fatal("[Database] Migration Error:", err) @@ -57,6 +60,9 @@ func DropTables() { &UserNotification{}, &UserFlag{}, &UserRole{}, + &OTSTemplate{}, + &RatingExamRecord{}, + &TrainingNotes{}, ) if err != nil { log.Fatal("[Database] Drop Table Error:", err) diff --git a/pkg/database/models/training_notes.go b/pkg/database/models/training_notes.go new file mode 100644 index 0000000..575c61c --- /dev/null +++ b/pkg/database/models/training_notes.go @@ -0,0 +1,46 @@ +package models + +import ( + "github.com/VATUSA/primary-api/pkg/constants" + "github.com/VATUSA/primary-api/pkg/database" + "time" +) + +type TrainingNotes struct { + ID uint `json:"id" gorm:"primaryKey" example:"1"` + StudentCID uint `json:"student_cid" example:"1293257" gorm:"index"` + Student *User `json:"-" gorm:"foreignKey:StudentCID"` + InstructorCID uint `json:"instructor_cid" example:"1293257"` + Instructor *User `json:"-" gorm:"foreignKey:InstructorCID"` + Facility constants.FacilityID `json:"facility" example:"ZDV"` + Position string `json:"position" example:"DEN_DEL"` + Duration time.Duration `json:"duration" example:"60"` + Notes string `json:"notes" example:"Great job!"` + RatingExamID uint `json:"rating_exam_id" example:"1"` + RatingExam *RatingExamRecord `json:"-" gorm:"foreignKey:RatingExamID"` + SessionDate time.Time `json:"session_date" example:"2021-01-01T00:00:00Z"` + CreatedAt time.Time `json:"created_at" example:"2021-01-01T00:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` +} + +func (tn *TrainingNotes) Create() error { + return database.DB.Create(tn).Error +} + +func (tn *TrainingNotes) Update() error { + return database.DB.Save(tn).Error +} + +func (tn *TrainingNotes) Delete() error { + return database.DB.Delete(tn).Error +} + +func (tn *TrainingNotes) Get() error { + return database.DB.Where("id = ?", tn.ID).First(tn).Error +} + +func GetFilteredTrainingNotes(filter map[string]interface{}) ([]TrainingNotes, error) { + var notes []TrainingNotes + err := database.DB.Preload("Student").Preload("Instructor").Preload("RatingExam").Where(filter).Find(¬es).Error + return notes, err +} diff --git a/pkg/database/models/training_ots.go b/pkg/database/models/training_ots.go new file mode 100644 index 0000000..dfaa6c3 --- /dev/null +++ b/pkg/database/models/training_ots.go @@ -0,0 +1,81 @@ +package models + +import ( + "errors" + "github.com/VATUSA/primary-api/pkg/database" + "gorm.io/datatypes" + "gorm.io/gorm" + "time" +) + +type RatingExamRecord struct { + ID uint `json:"id" gorm:"primaryKey"` + StudentCID uint `json:"student_cid" gorm:"index"` + InstructorCID uint `json:"instructor_cid" gorm:"index"` + Data datatypes.JSON `json:"data" gorm:"type:jsonb"` // Stores form fields dynamically + Notes string `json:"notes"` + Result bool `json:"result"` // True = Passed, False = Failed + CreatedAt time.Time `json:"created_at"` +} + +func (otsRecord *RatingExamRecord) BeforeCreate(tx *gorm.DB) error { + // check if student CID is valid + if !IsValidUser(otsRecord.StudentCID) { + return errors.New("invalid student CID") + } + + return nil +} + +func (otsRecord *RatingExamRecord) Create() error { + return database.DB.Create(otsRecord).Error +} + +func (otsRecord *RatingExamRecord) Update() error { + return database.DB.Updates(otsRecord).Error +} + +func (otsRecord *RatingExamRecord) Delete() error { + return database.DB.Delete(otsRecord).Error +} + +func (otsRecord *RatingExamRecord) Get() error { + return database.DB.Where("id = ?", otsRecord.ID).First(otsRecord).Error +} + +func GetFilteredOTSRecords(filter map[string]interface{}) ([]RatingExamRecord, error) { + var records []RatingExamRecord + err := database.DB.Where(filter).Find(&records).Error + return records, err +} + +type OTSTemplate struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name"` + Template datatypes.JSON `json:"template" gorm:"type:jsonb"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastUpdatedBy uint `json:"last_updated_by"` +} + +func (otsTemplates *OTSTemplate) Create() error { + return database.DB.Create(otsTemplates).Error +} + +func (otsTemplates *OTSTemplate) Update() error { + return database.DB.Updates(otsTemplates).Error +} + +func (otsTemplates *OTSTemplate) Delete() error { + return database.DB.Delete(otsTemplates).Error +} + +func (otsTemplates *OTSTemplate) Get() error { + return database.DB.Where("id = ?", otsTemplates.ID).First(otsTemplates).Error +} + +func GetAllOTSTemplates() ([]OTSTemplate, error) { + var templates []OTSTemplate + err := database.DB.Find(&templates).Error + return templates, err +} diff --git a/pkg/go-chi/middleware/auth/feedback.go b/pkg/go-chi/middleware/auth/feedback.go index 62e84a5..b921337 100644 --- a/pkg/go-chi/middleware/auth/feedback.go +++ b/pkg/go-chi/middleware/auth/feedback.go @@ -120,7 +120,7 @@ func CanLeaveFeedback(next http.Handler) http.Handler { if credentials.User != nil { if req.Status != types.Pending { - log.Error("User %d, attempted to create feedback with status: %s. No permissions.", credentials.User.CID, req.Status) + log.Errorf("User %d, attempted to create feedback with status: %s. No permissions.", credentials.User.CID, req.Status) utils.Render(w, r, utils.ErrForbidden) return } diff --git a/pkg/go-chi/middleware/auth/ots.go b/pkg/go-chi/middleware/auth/ots.go new file mode 100644 index 0000000..da59041 --- /dev/null +++ b/pkg/go-chi/middleware/auth/ots.go @@ -0,0 +1,53 @@ +package middleware + +import ( + "github.com/VATUSA/primary-api/pkg/utils" + log "github.com/sirupsen/logrus" + "net/http" +) + +func CanEditOTSTemplate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + log.Warnf("User %d, attempted to edit an OTS.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} + +func CanCROTSRecord(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetUser := utils.GetUserCtx(r) + + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + for _, roster := range targetUser.Roster { + if utils.IsFacilitySeniorStaff(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + + if utils.IsInstructor(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + } + + log.Warnf("User %d, attempted to create/read an OTS.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} diff --git a/pkg/go-chi/middleware/auth/roster.go b/pkg/go-chi/middleware/auth/roster.go index 345ba23..3943500 100644 --- a/pkg/go-chi/middleware/auth/roster.go +++ b/pkg/go-chi/middleware/auth/roster.go @@ -10,6 +10,8 @@ func CanEditRoster(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { targetFacility := utils.GetFacilityCtx(r) + roster := utils.GetRosterCtx(r) + credentials := GetCredentials(r) if credentials.User != nil { if utils.IsVATUSAStaff(credentials.User) { @@ -22,6 +24,11 @@ func CanEditRoster(next http.Handler) http.Handler { return } + if roster.Visiting && roster.CID == credentials.User.CID { + next.ServeHTTP(w, r) + return + } + log.Warnf("User %d, attempted to edit roster for facility: %s. No permissions.", credentials.User.CID, targetFacility.ID) } diff --git a/pkg/go-chi/middleware/auth/training-note.go b/pkg/go-chi/middleware/auth/training-note.go new file mode 100644 index 0000000..a724592 --- /dev/null +++ b/pkg/go-chi/middleware/auth/training-note.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "github.com/VATUSA/primary-api/pkg/utils" + log "github.com/sirupsen/logrus" + "net/http" +) + +func CanCreateReadTrainingNote(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetUser := utils.GetUserCtx(r) + + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + for _, roster := range targetUser.Roster { + if utils.IsFacilitySeniorStaff(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + + if utils.IsInstructor(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + + if utils.IsMentor(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + } + + log.Warnf("User %d, attempted to create/read a training note.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} + +func CanDeleteTrainingNote(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetUser := utils.GetUserCtx(r) + + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + for _, roster := range targetUser.Roster { + if utils.IsFacilitySeniorStaff(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + } + + log.Warnf("User %d, attempted to delete a training note.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} diff --git a/pkg/utils/context.go b/pkg/utils/context.go index 0404fb5..fb357b5 100644 --- a/pkg/utils/context.go +++ b/pkg/utils/context.go @@ -185,6 +185,36 @@ func GetRosterRequestCtx(r *http.Request) *models.RosterRequest { return rr } +type TrainingNoteKey struct{} + +func GetTrainingNoteCtx(r *http.Request) *models.TrainingNotes { + tn, ok := r.Context().Value(TrainingNoteKey{}).(*models.TrainingNotes) + if !ok { + return nil + } + return tn +} + +type OTSRecord struct{} + +func GetOTSRecordCtx(r *http.Request) *models.RatingExamRecord { + otsr, ok := r.Context().Value(OTSRecord{}).(*models.RatingExamRecord) + if !ok { + return nil + } + return otsr +} + +type OTSTemplateKey struct{} + +func GetOTSTemplateCtx(r *http.Request) *models.OTSTemplate { + otst, ok := r.Context().Value(OTSTemplateKey{}).(*models.OTSTemplate) + if !ok { + return nil + } + return otst +} + type UserNotificationKey struct{} func GetUserNotificationCtx(r *http.Request) *models.UserNotification { diff --git a/pkg/utils/perms.go b/pkg/utils/perms.go index dcc309a..708268c 100644 --- a/pkg/utils/perms.go +++ b/pkg/utils/perms.go @@ -109,3 +109,17 @@ func IsInstructor(user *models.User, facility constants.FacilityID) bool { return false } + +func IsMentor(user *models.User, facility constants.FacilityID) bool { + for _, roster := range user.Roster { + if roster.Facility == facility { + for _, roles := range roster.Roles { + if roles.RoleID == constants.MentorRole { + return true + } + } + } + } + + return false +} diff --git a/pkg/vatsim/api/base.go b/pkg/vatsim/api/base.go new file mode 100644 index 0000000..fd04162 --- /dev/null +++ b/pkg/vatsim/api/base.go @@ -0,0 +1,135 @@ +package vatsim_api + +import ( + "encoding/json" + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "time" +) + +const ( + baseURL = "https://api.vatsim.net/api" + baseV2URL = "https://api.vatsim.net/v2" + dataURL = "https://data.vatsim.net/v3/vatsim-data.json" +) + +type Location struct { + Region string `json:"region"` + Division string `json:"division"` + Subdivision string `json:"subdivision"` +} + +func GetLocation(cid uint) (Location, error) { + resp, err := http.Get(fmt.Sprintf("%s/ratings/%d/", baseURL, cid)) + if err != nil { + return Location{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return Location{}, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get division for %d: %s", cid, body) + return Location{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var division Location + err = json.Unmarshal(body, &division) + if err != nil { + return Location{}, err + } + + return division, nil +} + +type UserStats struct { + Atc float32 `json:"atc"` + Pilot float32 `json:"pilot"` + S1 float32 `json:"s1"` + S2 float32 `json:"s2"` + S3 float32 `json:"s3"` + C1 float32 `json:"c1"` + C2 float32 `json:"c2"` + C3 float32 `json:"c3"` + I1 float32 `json:"i1"` + I2 float32 `json:"i2"` + I3 float32 `json:"i3"` + Sup float32 `json:"sup"` + Adm float32 `json:"adm"` +} + +func GetHours(cid uint) (UserStats, error) { + resp, err := http.Get(fmt.Sprintf("%s/members/%d/stats", baseV2URL, cid)) + if err != nil { + return UserStats{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return UserStats{}, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get stats for %d: %s", cid, body) + return UserStats{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var userStats UserStats + err = json.Unmarshal(body, &userStats) + if err != nil { + return UserStats{}, err + } + + return userStats, nil +} + +type ATCConnections struct { + Items []struct { + ConnectionId struct { + Id int `json:"id"` + VatsimId string `json:"vatsim_id"` + Type int `json:"type"` + Rating int `json:"rating"` + Callsign string `json:"callsign"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + Server string `json:"server"` + } `json:"connection_id"` + AircraftTracked int `json:"aircrafttracked"` + AircraftSeen int `json:"aircraftseen"` + } `json:"items"` + Count uint `json:"count"` +} + +// GetATCConnections - for the last 30 days +func GetATCConnections(cid uint) (ATCConnections, error) { + resp, err := http.Get(fmt.Sprintf("%s/members/%d/atc", baseV2URL, cid)) + if err != nil { + return ATCConnections{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return ATCConnections{}, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get stats for %d: %s", cid, body) + return ATCConnections{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var atcConnections ATCConnections + err = json.Unmarshal(body, &atcConnections) + if err != nil { + return ATCConnections{}, err + } + + return atcConnections, nil +} diff --git a/pkg/vatusa/user/eligibility.go b/pkg/vatusa/user/eligibility.go new file mode 100644 index 0000000..041491e --- /dev/null +++ b/pkg/vatusa/user/eligibility.go @@ -0,0 +1,225 @@ +package user + +import ( + "github.com/VATUSA/primary-api/pkg/constants" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/database/types" + vatsim_api "github.com/VATUSA/primary-api/pkg/vatsim/api" + "time" +) + +// TODO 50/50 Rule + +type TransferEligibility struct { + IsVATUSAMember bool + NeedS1OrRCE bool + NoTransfersWithin90Days bool + NoOtherTransfers bool + NoPromotionsWithin90Days bool + NoStaffAtCurrentFacility bool + NotInstructor bool + UsedTransferWaiver bool +} + +func getTransferEligible(user *models.User) (*TransferEligibility, error) { + eligibility := &TransferEligibility{ + IsVATUSAMember: false, + } + + location, err := vatsim_api.GetLocation(user.CID) + if err != nil { + return nil, err + } + + if location.Division == "USA" { + eligibility.IsVATUSAMember = true + } + + // Needs RCE or S1 Exam + // TODO: Implement + + // No Transfers Within 60 Days + rosterReqs, err := models.GetFilteredRosterRequests(user.CID, types.Transferring, time.Now().AddDate(0, 0, -90)) + if err != nil { + return nil, err + } + + // No Other Transfers + for _, rosterReq := range rosterReqs { + if rosterReq.Status == types.Accepted { + eligibility.NoTransfersWithin90Days = false + } + + if rosterReq.Status == types.Pending { + eligibility.NoOtherTransfers = false + } + } + + // No Promotions Within 90 Days + ratingChanges, err := models.GetFilteredRatingChanges(user.CID, time.Now().Add(-90*24*time.Hour)) + if err != nil { + return nil, err + } + + for _, rc := range ratingChanges { + if rc.NewRating == constants.SeniorControllerRating || rc.OldRating == constants.SeniorControllerRating { + continue + } + if rc.NewRating == constants.InstructorRating || rc.OldRating == constants.InstructorRating { + continue + } + if rc.NewRating == constants.SeniorInstructorRating || rc.OldRating == constants.SeniorInstructorRating { + continue + } + if rc.NewRating == constants.SupervisorRating || rc.OldRating == constants.SupervisorRating { + continue + } + if rc.NewRating == constants.AdministratorRating || rc.OldRating == constants.AdministratorRating { + continue + } + + if rc.NewRating > rc.OldRating { + eligibility.NoPromotionsWithin90Days = false + break + } + } + + // No Staff at Current Facility + roles, err := models.GetAllUserRolesByCID(user.CID) + if err != nil { + return nil, err + } + + for _, role := range roles { + if role.RoleID.IsFacilityStaff() { + eligibility.NoStaffAtCurrentFacility = false + break + } + } + + // Not Instructor + if user.ControllerRating == constants.InstructorRating || user.ControllerRating == constants.SeniorInstructorRating { + eligibility.NotInstructor = false + } + + // Used Transfer Waiver + userFlags := &models.UserFlag{ + CID: user.CID, + } + if err := userFlags.Get(); err != nil { + return nil, err + } + + if userFlags.UsedTransferOverride { + eligibility.UsedTransferWaiver = true + } + + return eligibility, nil +} + +type VisitingEligibility struct { + HasS3 bool + NeedsRCE bool + NoVisitsWithin60Days bool + NoOtherVisitingRequests bool + NoPromotionsWithin90Days bool + Minimum50Hours bool + HasHomeFacility bool + VisitingAllowed bool +} + +func getVisitingEligible(user *models.User) (*VisitingEligibility, error) { + eligibility := &VisitingEligibility{ + HasS3: true, + NeedsRCE: true, + NoVisitsWithin60Days: true, + NoOtherVisitingRequests: true, + NoPromotionsWithin90Days: true, + Minimum50Hours: true, + HasHomeFacility: true, + VisitingAllowed: true, + } + + // Has S3 + if user.ControllerRating < constants.Student3Rating { + eligibility.HasS3 = false + } + + // Needs RCE + // TODO: Implement + + // No Visits Within 60 Days + rosterReqs, err := models.GetFilteredRosterRequests(user.CID, types.Visiting, time.Now().AddDate(0, 0, -60)) + if err != nil { + return nil, err + } + + for _, rosterReq := range rosterReqs { + if rosterReq.Status == types.Pending { + eligibility.NoOtherVisitingRequests = false + } + + if rosterReq.Status == types.Accepted { + eligibility.NoVisitsWithin60Days = false + } + } + + // No Promotions Within 90 Days + ratingChanges, err := models.GetFilteredRatingChanges(user.CID, time.Now().Add(-90*24*time.Hour)) + if err != nil { + return nil, err + } + + for _, rc := range ratingChanges { + if rc.NewRating == constants.SeniorControllerRating || rc.OldRating == constants.SeniorControllerRating { + continue + } + if rc.NewRating == constants.InstructorRating || rc.OldRating == constants.InstructorRating { + continue + } + if rc.NewRating == constants.SeniorInstructorRating || rc.OldRating == constants.SeniorInstructorRating { + continue + } + if rc.NewRating == constants.SupervisorRating || rc.OldRating == constants.SupervisorRating { + continue + } + if rc.NewRating == constants.AdministratorRating || rc.OldRating == constants.AdministratorRating { + continue + } + + if rc.NewRating > rc.OldRating { + eligibility.NoPromotionsWithin90Days = false + break + } + } + + // Minimum 50 Hours + hours, err := vatsim_api.GetHours(user.CID) + if err != nil { + return nil, err + } + + if user.ControllerRating == constants.Student1Rating && hours.S1 < 50 { + eligibility.Minimum50Hours = false + } else if user.ControllerRating == constants.Student2Rating && hours.S2 < 50 { + eligibility.Minimum50Hours = false + } else if user.ControllerRating == constants.Student3Rating && hours.S3 < 50 { + eligibility.Minimum50Hours = false + } else if user.ControllerRating == constants.ControllerRating && hours.C1 < 50 { + eligibility.Minimum50Hours = false + } + + // Visiting Allowed + userFlags := &models.UserFlag{ + CID: user.CID, + } + if err := userFlags.Get(); err != nil { + return nil, err + } + + if userFlags.NoVisiting { + eligibility.VisitingAllowed = false + } + + return eligibility, nil +} diff --git a/pkg/vatusa/user/hour_rule.go b/pkg/vatusa/user/hour_rule.go new file mode 100644 index 0000000..3dfbd37 --- /dev/null +++ b/pkg/vatusa/user/hour_rule.go @@ -0,0 +1,47 @@ +package user + +import ( + vatsim_api "github.com/VATUSA/primary-api/pkg/vatsim/api" + "github.com/VATUSA/primary-api/pkg/vnas" + log "github.com/sirupsen/logrus" + "time" +) + +func FiftyFiftyRuleCheck(homeARTCC string, cid uint) (bool, error) { + artcc, err := vnas.GetARTCC(homeARTCC) + if err != nil { + return false, err + } + + positions := artcc.Positions() + positionsMapped := make(map[string]bool, len(positions)) + for _, pos := range positions { + positionsMapped[pos] = true + } + + connections, err := vatsim_api.GetATCConnections(cid) + if err != nil { + return false, err + } + + var homeHours time.Duration + var otherHours time.Duration + + for _, connection := range connections.Items { + if time.Since(connection.ConnectionId.End) > 30*24*time.Hour { + break + } + hours := connection.ConnectionId.End.Sub(connection.ConnectionId.Start) + + if positionsMapped[connection.ConnectionId.Callsign] { + homeHours += hours + } else { + otherHours += hours + } + } + + log.Debug("Home hours: ", homeHours) + log.Debug("Other hours: ", otherHours) + + return homeHours > otherHours, nil +} diff --git a/pkg/vnas/base.go b/pkg/vnas/base.go new file mode 100644 index 0000000..e975408 --- /dev/null +++ b/pkg/vnas/base.go @@ -0,0 +1,73 @@ +package vnas + +import ( + "encoding/json" + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "time" +) + +const ( + baseURL = "https://data-api.vnas.vatsim.net/api" +) + +type ARTCC struct { + ID string `json:"id"` + Facility Facility `json:"facility"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} + +type Facility struct { + ID string `json:"id" example:"ZDV"` + Name string `json:"name" example:"Denver ARTCC"` + Positions []Position `json:"positions"` + ChildFacilities []Facility `json:"childFacilities"` +} + +type Position struct { + ID string `json:"id" example:""` + Name string `json:"name" example:"14 - Hayden High"` + Callsign string `json:"callsign" example:"DEN_14_CTR"` +} + +func GetARTCC(id string) (*ARTCC, error) { + resp, err := http.Get(fmt.Sprintf("%s/artccs/%s", baseURL, id)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get ARTCC for %s: %s", id, body) + return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var artcc ARTCC + if err = json.Unmarshal(body, &artcc); err != nil { + return nil, err + } + + return &artcc, nil +} + +func (artcc ARTCC) Positions() []string { + var positions []string + for _, position := range artcc.Facility.Positions { + positions = append(positions, position.Callsign) + } + + for _, facility := range artcc.Facility.ChildFacilities { + for _, position := range facility.Positions { + positions = append(positions, position.Callsign) + } + } + + return positions +} diff --git a/views/docs/docs.go b/views/docs/docs.go index b27549f..24ea479 100644 --- a/views/docs/docs.go +++ b/views/docs/docs.go @@ -3127,6 +3127,239 @@ const docTemplate = `{ } } }, + "/facility/{FacilityID}/training/notes": { + "get": { + "description": "List all training notes for a specific facility", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a Facility", + "parameters": [ + { + "type": "string", + "description": "Facility ID", + "name": "FacilityID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/training/ots/templates": { + "get": { + "description": "List all OTS templates", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List all OTS templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create an OTS template", + "parameters": [ + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.otsTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/training/ots/templates/{id}": { + "delete": { + "description": "Delete an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Delete an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "patch": { + "description": "Patch an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Patch an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.otsTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, "/user/": { "get": { "description": "Get information for the user logged in", @@ -4541,8 +4774,345 @@ const docTemplate = `{ "$ref": "#/definitions/user_role.Response" } }, - "400": { - "description": "Bad Request", + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Create a new user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "User Role", + "name": "user_role", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user_role.Request" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/user_role.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roles/{role_id}": { + "delete": { + "description": "Remove a user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Remove a user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Role ID", + "name": "role_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roster": { + "get": { + "description": "Get rosters by user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "roster" + ], + "summary": "Get rosters by user", + "parameters": [ + { + "type": "integer", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/roster.Response" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes": { + "get": { + "description": "List all training notes for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a user", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new training note", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create a new training note", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "Training Note", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.noteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes/{note_id}": { + "get": { + "description": "Retrieve a specific training note by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Get a specific training note", + "parameters": [ + { + "type": "integer", + "description": "Note ID", + "name": "note_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/utils.ErrResponse" } @@ -4555,8 +5125,8 @@ const docTemplate = `{ } } }, - "post": { - "description": "Create a new user role", + "delete": { + "description": "Delete a specific training note by its ID", "consumes": [ "application/json" ], @@ -4564,33 +5134,28 @@ const docTemplate = `{ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Create a new user role", + "summary": "Delete a specific training note", "parameters": [ { "type": "integer", - "description": "User CID", - "name": "cid", + "description": "Note ID", + "name": "note_id", "in": "path", "required": true }, { - "description": "User Role", - "name": "user_role", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user_role.Request" - } + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/user_role.Response" - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -4598,6 +5163,24 @@ const docTemplate = `{ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -4607,9 +5190,9 @@ const docTemplate = `{ } } }, - "/user/{cid}/roles/{role_id}": { - "delete": { - "description": "Remove a user role", + "/user/{cid}/training/ots": { + "get": { + "description": "List all OTS Records for user", "consumes": [ "application/json" ], @@ -4617,31 +5200,36 @@ const docTemplate = `{ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Remove a user role", + "summary": "List all OTS Records for user", "parameters": [ - { - "type": "integer", - "description": "User CID", - "name": "cid", - "in": "path", - "required": true - }, { "type": "string", - "description": "Role ID", - "name": "role_id", + "description": "CID", + "name": "cid", "in": "path", "required": true } ], "responses": { - "204": { - "description": "No Content" + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.otsRecordResponse" + } + } }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", "schema": { "$ref": "#/definitions/utils.ErrResponse" } @@ -4653,11 +5241,9 @@ const docTemplate = `{ } } } - } - }, - "/user/{cid}/roster": { - "get": { - "description": "Get rosters by user", + }, + "post": { + "description": "Create a new OTS Record", "consumes": [ "application/json" ], @@ -4665,26 +5251,32 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roster" + "training" ], - "summary": "Get rosters by user", + "summary": "Create a new OTS Record", "parameters": [ { - "type": "integer", + "type": "string", "description": "CID", "name": "cid", "in": "path", "required": true + }, + { + "description": "OTS Record", + "name": "otsRecordRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.otsRecordRequest" + } } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/roster.Response" - } + "$ref": "#/definitions/trainingrecords.otsRecordResponse" } }, "400": { @@ -4693,6 +5285,18 @@ const docTemplate = `{ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -6389,7 +6993,6 @@ const docTemplate = `{ "example": "2021-01-01T00:00:00Z" }, "deleted_at": { - "description": "Soft Deletes for logging", "type": "string", "example": "2021-01-01T00:00:00Z" }, @@ -6523,6 +7126,201 @@ const docTemplate = `{ } } }, + "time.Duration": { + "type": "integer", + "enum": [ + 1, + 1000, + 1000000, + 1000000000 + ], + "x-enum-varnames": [ + "Nanosecond", + "Microsecond", + "Millisecond", + "Second" + ] + }, + "training.otsTemplateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "training.otsTemplateResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_by": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + }, + "updated_at": { + "type": "string" + } + } + }, + "trainingrecords.noteRequest": { + "type": "object", + "properties": { + "duration": { + "$ref": "#/definitions/time.Duration" + }, + "facility": { + "$ref": "#/definitions/constants.FacilityID" + }, + "notes": { + "type": "string" + }, + "position": { + "type": "string" + }, + "rating_exam_id": { + "type": "integer" + }, + "session_date": { + "type": "string" + } + } + }, + "trainingrecords.noteResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/time.Duration" + } + ], + "example": 60 + }, + "facility": { + "allOf": [ + { + "$ref": "#/definitions/constants.FacilityID" + } + ], + "example": "ZDV" + }, + "id": { + "type": "integer", + "example": 1 + }, + "instructor_cid": { + "type": "integer", + "example": 1293257 + }, + "instructor_name": { + "type": "string" + }, + "notes": { + "type": "string", + "example": "Great job!" + }, + "position": { + "type": "string", + "example": "DEN_DEL" + }, + "rating_exam_id": { + "type": "integer", + "example": 1 + }, + "rating_exam_notes": { + "type": "string" + }, + "rating_exam_result": { + "type": "boolean" + }, + "session_date": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "student_cid": { + "type": "integer", + "example": 1293257 + }, + "student_name": { + "type": "string" + }, + "updated_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + } + } + }, + "trainingrecords.otsRecordRequest": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "notes": { + "type": "string" + }, + "result": { + "type": "boolean" + } + } + }, + "trainingrecords.otsRecordResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "data": { + "description": "Stores form fields dynamically", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "instructor_cid": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "result": { + "description": "True = Passed, False = Failed", + "type": "boolean" + }, + "student_cid": { + "type": "integer" + } + } + }, "types.FeedbackRating": { "type": "string", "enum": [ @@ -6851,7 +7649,7 @@ const docTemplate = `{ ], "example": "ZDV" }, - "role_id": { + "role": { "allOf": [ { "$ref": "#/definitions/constants.RoleID" diff --git a/views/docs/swagger.json b/views/docs/swagger.json index f2c2feb..09252f2 100644 --- a/views/docs/swagger.json +++ b/views/docs/swagger.json @@ -3120,6 +3120,239 @@ } } }, + "/facility/{FacilityID}/training/notes": { + "get": { + "description": "List all training notes for a specific facility", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a Facility", + "parameters": [ + { + "type": "string", + "description": "Facility ID", + "name": "FacilityID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/training/ots/templates": { + "get": { + "description": "List all OTS templates", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List all OTS templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create an OTS template", + "parameters": [ + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.otsTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/training/ots/templates/{id}": { + "delete": { + "description": "Delete an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Delete an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "patch": { + "description": "Patch an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Patch an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.otsTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, "/user/": { "get": { "description": "Get information for the user logged in", @@ -4534,8 +4767,345 @@ "$ref": "#/definitions/user_role.Response" } }, - "400": { - "description": "Bad Request", + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Create a new user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "User Role", + "name": "user_role", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user_role.Request" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/user_role.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roles/{role_id}": { + "delete": { + "description": "Remove a user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Remove a user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Role ID", + "name": "role_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roster": { + "get": { + "description": "Get rosters by user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "roster" + ], + "summary": "Get rosters by user", + "parameters": [ + { + "type": "integer", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/roster.Response" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes": { + "get": { + "description": "List all training notes for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a user", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new training note", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create a new training note", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "Training Note", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.noteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes/{note_id}": { + "get": { + "description": "Retrieve a specific training note by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Get a specific training note", + "parameters": [ + { + "type": "integer", + "description": "Note ID", + "name": "note_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/utils.ErrResponse" } @@ -4548,8 +5118,8 @@ } } }, - "post": { - "description": "Create a new user role", + "delete": { + "description": "Delete a specific training note by its ID", "consumes": [ "application/json" ], @@ -4557,33 +5127,28 @@ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Create a new user role", + "summary": "Delete a specific training note", "parameters": [ { "type": "integer", - "description": "User CID", - "name": "cid", + "description": "Note ID", + "name": "note_id", "in": "path", "required": true }, { - "description": "User Role", - "name": "user_role", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user_role.Request" - } + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/user_role.Response" - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -4591,6 +5156,24 @@ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -4600,9 +5183,9 @@ } } }, - "/user/{cid}/roles/{role_id}": { - "delete": { - "description": "Remove a user role", + "/user/{cid}/training/ots": { + "get": { + "description": "List all OTS Records for user", "consumes": [ "application/json" ], @@ -4610,31 +5193,36 @@ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Remove a user role", + "summary": "List all OTS Records for user", "parameters": [ - { - "type": "integer", - "description": "User CID", - "name": "cid", - "in": "path", - "required": true - }, { "type": "string", - "description": "Role ID", - "name": "role_id", + "description": "CID", + "name": "cid", "in": "path", "required": true } ], "responses": { - "204": { - "description": "No Content" + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.otsRecordResponse" + } + } }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", "schema": { "$ref": "#/definitions/utils.ErrResponse" } @@ -4646,11 +5234,9 @@ } } } - } - }, - "/user/{cid}/roster": { - "get": { - "description": "Get rosters by user", + }, + "post": { + "description": "Create a new OTS Record", "consumes": [ "application/json" ], @@ -4658,26 +5244,32 @@ "application/json" ], "tags": [ - "roster" + "training" ], - "summary": "Get rosters by user", + "summary": "Create a new OTS Record", "parameters": [ { - "type": "integer", + "type": "string", "description": "CID", "name": "cid", "in": "path", "required": true + }, + { + "description": "OTS Record", + "name": "otsRecordRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.otsRecordRequest" + } } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/roster.Response" - } + "$ref": "#/definitions/trainingrecords.otsRecordResponse" } }, "400": { @@ -4686,6 +5278,18 @@ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -6382,7 +6986,6 @@ "example": "2021-01-01T00:00:00Z" }, "deleted_at": { - "description": "Soft Deletes for logging", "type": "string", "example": "2021-01-01T00:00:00Z" }, @@ -6516,6 +7119,201 @@ } } }, + "time.Duration": { + "type": "integer", + "enum": [ + 1, + 1000, + 1000000, + 1000000000 + ], + "x-enum-varnames": [ + "Nanosecond", + "Microsecond", + "Millisecond", + "Second" + ] + }, + "training.otsTemplateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "training.otsTemplateResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_by": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + }, + "updated_at": { + "type": "string" + } + } + }, + "trainingrecords.noteRequest": { + "type": "object", + "properties": { + "duration": { + "$ref": "#/definitions/time.Duration" + }, + "facility": { + "$ref": "#/definitions/constants.FacilityID" + }, + "notes": { + "type": "string" + }, + "position": { + "type": "string" + }, + "rating_exam_id": { + "type": "integer" + }, + "session_date": { + "type": "string" + } + } + }, + "trainingrecords.noteResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/time.Duration" + } + ], + "example": 60 + }, + "facility": { + "allOf": [ + { + "$ref": "#/definitions/constants.FacilityID" + } + ], + "example": "ZDV" + }, + "id": { + "type": "integer", + "example": 1 + }, + "instructor_cid": { + "type": "integer", + "example": 1293257 + }, + "instructor_name": { + "type": "string" + }, + "notes": { + "type": "string", + "example": "Great job!" + }, + "position": { + "type": "string", + "example": "DEN_DEL" + }, + "rating_exam_id": { + "type": "integer", + "example": 1 + }, + "rating_exam_notes": { + "type": "string" + }, + "rating_exam_result": { + "type": "boolean" + }, + "session_date": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "student_cid": { + "type": "integer", + "example": 1293257 + }, + "student_name": { + "type": "string" + }, + "updated_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + } + } + }, + "trainingrecords.otsRecordRequest": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "notes": { + "type": "string" + }, + "result": { + "type": "boolean" + } + } + }, + "trainingrecords.otsRecordResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "data": { + "description": "Stores form fields dynamically", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "instructor_cid": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "result": { + "description": "True = Passed, False = Failed", + "type": "boolean" + }, + "student_cid": { + "type": "integer" + } + } + }, "types.FeedbackRating": { "type": "string", "enum": [ @@ -6844,7 +7642,7 @@ ], "example": "ZDV" }, - "role_id": { + "role": { "allOf": [ { "$ref": "#/definitions/constants.RoleID" diff --git a/views/docs/swagger.yaml b/views/docs/swagger.yaml index 30c619d..ee6d9c5 100644 --- a/views/docs/swagger.yaml +++ b/views/docs/swagger.yaml @@ -1083,7 +1083,6 @@ definitions: example: "2021-01-01T00:00:00Z" type: string deleted_at: - description: Soft Deletes for logging example: "2021-01-01T00:00:00Z" type: string facility: @@ -1175,6 +1174,137 @@ definitions: example: "2021-01-01T00:00:00Z" type: string type: object + time.Duration: + enum: + - 1 + - 1000 + - 1000000 + - 1000000000 + type: integer + x-enum-varnames: + - Nanosecond + - Microsecond + - Millisecond + - Second + training.otsTemplateRequest: + properties: + name: + type: string + template: + items: + type: integer + type: array + type: object + training.otsTemplateResponse: + properties: + created_at: + type: string + id: + type: integer + last_updated_by: + type: integer + name: + type: string + template: + items: + type: integer + type: array + updated_at: + type: string + type: object + trainingrecords.noteRequest: + properties: + duration: + $ref: '#/definitions/time.Duration' + facility: + $ref: '#/definitions/constants.FacilityID' + notes: + type: string + position: + type: string + rating_exam_id: + type: integer + session_date: + type: string + type: object + trainingrecords.noteResponse: + properties: + created_at: + example: "2021-01-01T00:00:00Z" + type: string + duration: + allOf: + - $ref: '#/definitions/time.Duration' + example: 60 + facility: + allOf: + - $ref: '#/definitions/constants.FacilityID' + example: ZDV + id: + example: 1 + type: integer + instructor_cid: + example: 1293257 + type: integer + instructor_name: + type: string + notes: + example: Great job! + type: string + position: + example: DEN_DEL + type: string + rating_exam_id: + example: 1 + type: integer + rating_exam_notes: + type: string + rating_exam_result: + type: boolean + session_date: + example: "2021-01-01T00:00:00Z" + type: string + student_cid: + example: 1293257 + type: integer + student_name: + type: string + updated_at: + example: "2021-01-01T00:00:00Z" + type: string + type: object + trainingrecords.otsRecordRequest: + properties: + data: + items: + type: integer + type: array + notes: + type: string + result: + type: boolean + type: object + trainingrecords.otsRecordResponse: + properties: + created_at: + type: string + data: + description: Stores form fields dynamically + items: + type: integer + type: array + id: + type: integer + instructor_cid: + type: integer + notes: + type: string + result: + description: True = Passed, False = Failed + type: boolean + student_cid: + type: integer + type: object types.FeedbackRating: enum: - unsatisfactory @@ -1408,7 +1538,7 @@ definitions: allOf: - $ref: '#/definitions/constants.FacilityID' example: ZDV - role_id: + role: allOf: - $ref: '#/definitions/constants.RoleID' example: ATM @@ -3511,6 +3641,160 @@ paths: summary: Delete a roster tags: - roster + /facility/{FacilityID}/training/notes: + get: + consumes: + - application/json + description: List all training notes for a specific facility + parameters: + - description: Facility ID + in: path + name: FacilityID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/trainingrecords.noteResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List training notes for a Facility + tags: + - training + /training/ots/templates: + get: + consumes: + - application/json + description: List all OTS templates + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/training.otsTemplateResponse' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List all OTS templates + tags: + - training + post: + consumes: + - application/json + description: Create an OTS template + parameters: + - description: Template + in: body + name: template + required: true + schema: + $ref: '#/definitions/training.otsTemplateRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/training.otsTemplateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Create an OTS template + tags: + - training + /training/ots/templates/{id}: + delete: + consumes: + - application/json + description: Delete an OTS template + parameters: + - description: Template ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "204": + description: No Content + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Delete an OTS template + tags: + - training + patch: + consumes: + - application/json + description: Patch an OTS template + parameters: + - description: Template ID + in: path + name: id + required: true + type: integer + - description: Template + in: body + name: template + required: true + schema: + $ref: '#/definitions/training.otsTemplateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/training.otsTemplateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Patch an OTS template + tags: + - training /user/: get: consumes: @@ -4531,6 +4815,253 @@ paths: summary: Get rosters by user tags: - roster + /user/{cid}/training/notes: + get: + consumes: + - application/json + description: List all training notes for a specific user + parameters: + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/trainingrecords.noteResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List training notes for a user + tags: + - training + post: + consumes: + - application/json + description: Create a new training note + parameters: + - description: CID + in: path + name: cid + required: true + type: string + - description: Training Note + in: body + name: note + required: true + schema: + $ref: '#/definitions/trainingrecords.noteRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/trainingrecords.noteResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Create a new training note + tags: + - training + /user/{cid}/training/notes/{note_id}: + delete: + consumes: + - application/json + description: Delete a specific training note by its ID + parameters: + - description: Note ID + in: path + name: note_id + required: true + type: integer + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Delete a specific training note + tags: + - training + get: + consumes: + - application/json + description: Retrieve a specific training note by its ID + parameters: + - description: Note ID + in: path + name: note_id + required: true + type: integer + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/trainingrecords.noteResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Get a specific training note + tags: + - training + /user/{cid}/training/ots: + get: + consumes: + - application/json + description: List all OTS Records for user + parameters: + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/trainingrecords.otsRecordResponse' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List all OTS Records for user + tags: + - training + post: + consumes: + - application/json + description: Create a new OTS Record + parameters: + - description: CID + in: path + name: cid + required: true + type: string + - description: OTS Record + in: body + name: otsRecordRequest + required: true + schema: + $ref: '#/definitions/trainingrecords.otsRecordRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/trainingrecords.otsRecordResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Create a new OTS Record + tags: + - training /user/{cid}/user-flag: delete: consumes: diff --git a/views/router.go b/views/router.go index 37acc27..a6e69ee 100644 --- a/views/router.go +++ b/views/router.go @@ -31,7 +31,7 @@ import ( // @name x-api-key func Router(r chi.Router, cfg *config.Config) { - v3.Router(r, cfg) + v3.Router(r) docs.SwaggerInfo.Host = cfg.API.BaseURL[strings.Index(cfg.API.BaseURL, "://")+3:] diff --git a/views/v3/facility/router.go b/views/v3/facility/router.go index c775e30..2bc16ad 100644 --- a/views/v3/facility/router.go +++ b/views/v3/facility/router.go @@ -13,6 +13,7 @@ import ( "github.com/VATUSA/primary-api/views/v3/news" "github.com/VATUSA/primary-api/views/v3/roster" roster_request "github.com/VATUSA/primary-api/views/v3/roster-request" + trainingrecords "github.com/VATUSA/primary-api/views/v3/training-records" "github.com/go-chi/chi/v5" "net/http" ) @@ -60,6 +61,8 @@ func Router(r chi.Router) { r.Route("/roster-request", func(r chi.Router) { roster_request.Router(r) }) + + r.Get("/training/notes", trainingrecords.ListNotesForFac) }) } diff --git a/views/v3/rating-change/rating_change.go b/views/v3/rating-change/rating_change.go index e514912..c59e935 100644 --- a/views/v3/rating-change/rating_change.go +++ b/views/v3/rating-change/rating_change.go @@ -10,6 +10,7 @@ import ( "github.com/go-playground/validator/v10" "net/http" "sort" + "time" ) type Request struct { @@ -115,7 +116,7 @@ func CreateRatingChange(w http.ResponseWriter, r *http.Request) { // @Failure 500 {object} utils.ErrResponse // @Router /user/{cid}/rating-change [get] func ListRatingChanges(w http.ResponseWriter, r *http.Request) { - rc, err := models.GetAllRatingChangesByCID(utils.GetUserCtx(r).CID) + rc, err := models.GetFilteredRatingChanges(utils.GetUserCtx(r).CID, time.Time{}) if err != nil { utils.Render(w, r, utils.ErrInvalidRequest(err)) return diff --git a/views/v3/roster-request/roster_request.go b/views/v3/roster-request/roster_request.go index cd678e4..18f2042 100644 --- a/views/v3/roster-request/roster_request.go +++ b/views/v3/roster-request/roster_request.go @@ -211,27 +211,6 @@ func PatchRosterRequest(w http.ResponseWriter, r *http.Request) { } if data.Status != "" { - if req.Status == types.Pending && data.Status == types.Accepted { - roster := &models.Roster{ - CID: req.CID, - Facility: req.Facility, - OIs: "", - Home: false, - Visiting: false, - Status: "Active", - } - - if data.RequestType == types.Visiting { - roster.Visiting = true - } else { - roster.Home = true - } - - if err := roster.Create(); err != nil { - utils.Render(w, r, utils.ErrInvalidRequest(err)) - return - } - } req.Status = data.Status } diff --git a/views/v3/router.go b/views/v3/router.go index efcc690..3aad5e5 100644 --- a/views/v3/router.go +++ b/views/v3/router.go @@ -1,14 +1,14 @@ package v3 import ( - "github.com/VATUSA/primary-api/pkg/config" "github.com/VATUSA/primary-api/views/v3/event" "github.com/VATUSA/primary-api/views/v3/facility" + "github.com/VATUSA/primary-api/views/v3/training" "github.com/VATUSA/primary-api/views/v3/user" "github.com/go-chi/chi/v5" ) -func Router(r chi.Router, cfg *config.Config) { +func Router(r chi.Router) { r.Route("/v3", func(r chi.Router) { r.Route("/user", func(r chi.Router) { user.Router(r) @@ -19,5 +19,9 @@ func Router(r chi.Router, cfg *config.Config) { }) r.Get("/events", event.GetAllEvents) + + r.Route("/training", func(r chi.Router) { + training.Router(r) + }) }) } diff --git a/views/v3/training-records/note.go b/views/v3/training-records/note.go new file mode 100644 index 0000000..6a22a7c --- /dev/null +++ b/views/v3/training-records/note.go @@ -0,0 +1,218 @@ +package trainingrecords + +import ( + "encoding/json" + "errors" + "github.com/VATUSA/primary-api/pkg/constants" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" + "net/http" + "time" +) + +type noteRequest struct { + Facility constants.FacilityID `json:"facility"` + Position string `json:"position"` + Duration time.Duration `json:"duration"` + Notes string `json:"notes"` + RatingExamID uint `json:"rating_exam_id"` + SessionDate time.Time `json:"session_date"` +} + +func (req *noteRequest) Validate() error { + return validator.New().Struct(req) +} + +func (req *noteRequest) Bind(r *http.Request) error { + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return err + } + return nil +} + +type noteResponse struct { + *models.TrainingNotes + StudentName string `json:"student_name"` + InstructorName string `json:"instructor_name"` + RatingExamNotes string `json:"rating_exam_notes"` + RatingExamResult bool `json:"rating_exam_result"` +} + +func newNoteResponse(note *models.TrainingNotes) *noteResponse { + resp := ¬eResponse{ + TrainingNotes: note, + StudentName: note.Student.FirstName + " " + note.Student.LastName, + InstructorName: note.Instructor.FirstName + " " + note.Instructor.LastName, + } + + if note.RatingExam != nil { + resp.RatingExamNotes = note.RatingExam.Notes + resp.RatingExamResult = note.RatingExam.Result + } + + return resp +} + +func (res *noteResponse) Render(w http.ResponseWriter, r *http.Request) error { + if res.TrainingNotes == nil { + return errors.New("training note not found") + } + return nil +} + +func newNoteListResponse(notes []models.TrainingNotes) []render.Renderer { + list := []render.Renderer{} + for idx := range notes { + list = append(list, newNoteResponse(¬es[idx])) + } + return list +} + +// createNote godoc +// @Summary Create a new training note +// @Description Create a new training note +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Param note body noteRequest true "Training Note" +// @Success 201 {object} noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes [post] +func createNote(w http.ResponseWriter, r *http.Request) { + data := ¬eRequest{} + if err := render.Bind(r, data); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + note := &models.TrainingNotes{ + Facility: data.Facility, + Position: data.Position, + Duration: data.Duration, + Notes: data.Notes, + RatingExamID: data.RatingExamID, + SessionDate: data.SessionDate, + } + + note.StudentCID = utils.GetUserCtx(r).CID + note.InstructorCID = utils.GetXUser(r).CID + + if err := note.Create(); err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + render.Status(r, http.StatusCreated) + utils.Render(w, r, newNoteResponse(note)) +} + +// listNotes godoc +// @Summary List training notes for a user +// @Description List all training notes for a specific user +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Success 200 {object} []noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes [get] +func listNotes(w http.ResponseWriter, r *http.Request) { + cid := utils.GetUserCtx(r).CID + + // Fetch the training notes for the user + notes, err := models.GetFilteredTrainingNotes(map[string]interface{}{"student_cid": cid}) + if err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + if err := render.RenderList(w, r, newNoteListResponse(notes)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// ListNotesForFac godoc +// @Summary List training notes for a Facility +// @Description List all training notes for a specific facility +// @Tags training +// @Accept json +// @Produce json +// @Param FacilityID path string true "Facility ID" +// @Success 200 {object} []noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /facility/{FacilityID}/training/notes [get] +func ListNotesForFac(w http.ResponseWriter, r *http.Request) { + fac := utils.GetFacilityCtx(r) + + // Fetch the training notes for the facility + notes, err := models.GetFilteredTrainingNotes(map[string]interface{}{"facility": fac.ID}) + if err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + if err := render.RenderList(w, r, newNoteListResponse(notes)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// getNote godoc +// @Summary Get a specific training note +// @Description Retrieve a specific training note by its ID +// @Tags training +// @Accept json +// @Produce json +// @Param note_id path uint true "Note ID" +// @Param cid path string true "CID" +// @Success 200 {object} noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 404 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes/{note_id} [get] +func getNote(w http.ResponseWriter, r *http.Request) { + note := utils.GetTrainingNoteCtx(r) + + utils.Render(w, r, newNoteResponse(note)) +} + +// deleteNote godoc +// @Summary Delete a specific training note +// @Description Delete a specific training note by its ID +// @Tags training +// @Accept json +// @Produce json +// @Param note_id path uint true "Note ID" +// @Param cid path string true "CID" +// @Success 204 "No Content" +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 404 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes/{note_id} [delete] +func deleteNote(w http.ResponseWriter, r *http.Request) { + note := utils.GetTrainingNoteCtx(r) + + if err := note.Delete(); err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + render.Status(r, http.StatusNoContent) // HTTP 204 +} diff --git a/views/v3/training-records/ots_record.go b/views/v3/training-records/ots_record.go new file mode 100644 index 0000000..2af8175 --- /dev/null +++ b/views/v3/training-records/ots_record.go @@ -0,0 +1,121 @@ +package trainingrecords + +import ( + "encoding/json" + "errors" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" + "gorm.io/datatypes" + "net/http" +) + +type otsRecordRequest struct { + Data datatypes.JSON `json:"data"` + Notes string `json:"notes"` + Result bool `json:"result"` +} + +func (req *otsRecordRequest) Validate() error { + return validator.New().Struct(req) +} + +func (req *otsRecordRequest) Bind(r *http.Request) error { + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return err + } + return nil +} + +type otsRecordResponse struct { + *models.RatingExamRecord +} + +func newOTSRecordResponse(temp *models.RatingExamRecord) *otsRecordResponse { + return &otsRecordResponse{temp} +} + +func (res *otsRecordResponse) Render(w http.ResponseWriter, r *http.Request) error { + if res.RatingExamRecord == nil { + return errors.New("OTS Record not found") + } + return nil +} + +func newOTSRecordList(templates []models.RatingExamRecord) []render.Renderer { + list := []render.Renderer{} + for idx := range templates { + list = append(list, newOTSRecordResponse(&templates[idx])) + } + return list +} + +// createOTSRecord godoc +// @Summary Create a new OTS Record +// @Description Create a new OTS Record +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Param otsRecordRequest body otsRecordRequest true "OTS Record" +// @Success 201 {object} otsRecordResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/ots [post] +func createOTSRecord(w http.ResponseWriter, r *http.Request) { + cid := utils.GetUserCtx(r).CID + data := &otsRecordRequest{} + if err := render.Bind(r, data); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + record := &models.RatingExamRecord{ + StudentCID: cid, + Data: data.Data, + Notes: data.Notes, + Result: data.Result, + } + + instructorCID := utils.GetXUser(r).CID + record.InstructorCID = instructorCID + + if err := record.Create(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + if err := render.Render(w, r, newOTSRecordResponse(record)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// listOTSRecords godoc +// @Summary List all OTS Records for user +// @Description List all OTS Records for user +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Success 200 {object} []otsRecordResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/ots [get] +func listOTSRecords(w http.ResponseWriter, r *http.Request) { + cid := utils.GetUserCtx(r).CID + records, err := models.GetFilteredOTSRecords(map[string]interface{}{"student_cid": cid}) + if err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + if err := render.RenderList(w, r, newOTSRecordList(records)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} diff --git a/views/v3/training-records/router.go b/views/v3/training-records/router.go new file mode 100644 index 0000000..8f17b0e --- /dev/null +++ b/views/v3/training-records/router.go @@ -0,0 +1,58 @@ +package trainingrecords + +import ( + "context" + "github.com/VATUSA/primary-api/pkg/database/models" + middleware "github.com/VATUSA/primary-api/pkg/go-chi/middleware/auth" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/chi/v5" + "net/http" + "strconv" +) + +func Router(r chi.Router) { + r.Use(middleware.NotGuest) + + r.Route("/ots", func(r chi.Router) { + r.Use(middleware.CanCROTSRecord) + r.Get("/", listOTSRecords) + r.Post("/", createOTSRecord) + }) + + r.Route("/notes", func(r chi.Router) { + r.Use(middleware.CanCreateReadTrainingNote) + r.Get("/", listNotes) + r.Post("/", createNote) + + r.Route("/{NoteID}", func(r chi.Router) { + r.Use(noteCtx) + r.Get("/", getNote) + r.With(middleware.CanDeleteTrainingNote).Delete("/", deleteNote) + }) + }) +} + +func noteCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "NoteID") + if id == "" { + http.Error(w, "Invalid training note ID", http.StatusBadRequest) + return + } + + noteID, err := strconv.ParseUint(id, 10, 64) + if err != nil { + http.Error(w, "Invalid training note ID", http.StatusBadRequest) + return + } + + note := &models.TrainingNotes{ID: uint(noteID)} + if err = note.Get(); err != nil { + http.Error(w, "Invalid training note ID", http.StatusBadRequest) + return + } + + ctx := context.WithValue(r.Context(), utils.TrainingNoteKey{}, note) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/views/v3/training/ots_templates.go b/views/v3/training/ots_templates.go new file mode 100644 index 0000000..8280c2e --- /dev/null +++ b/views/v3/training/ots_templates.go @@ -0,0 +1,180 @@ +package training + +import ( + "encoding/json" + "errors" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" + "gorm.io/datatypes" + "net/http" +) + +type otsTemplateRequest struct { + Name string `json:"name"` + Template json.RawMessage `json:"template"` +} + +func (req *otsTemplateRequest) Validate() error { + return validator.New().Struct(req) +} + +func (req *otsTemplateRequest) Bind(r *http.Request) error { + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return err + } + return nil +} + +type otsTemplateResponse struct { + *models.OTSTemplate +} + +func newOTSTemplateResponse(temp *models.OTSTemplate) *otsTemplateResponse { + return &otsTemplateResponse{temp} +} + +func (res *otsTemplateResponse) Render(w http.ResponseWriter, r *http.Request) error { + if res.OTSTemplate == nil { + return errors.New("OTS Template not found") + } + return nil +} + +func newTemplateList(templates []models.OTSTemplate) []render.Renderer { + list := []render.Renderer{} + for idx := range templates { + list = append(list, newOTSTemplateResponse(&templates[idx])) + } + return list +} + +// createTemplate godoc +// @Summary Create an OTS template +// @Description Create an OTS template +// @Tags training +// @Accept json +// @Produce json +// @Param template body otsTemplateRequest true "Template" +// @Success 201 {object} otsTemplateResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates [post] +func createTemplate(w http.ResponseWriter, r *http.Request) { + data := &otsTemplateRequest{} + if err := data.Bind(r); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + template := &models.OTSTemplate{ + Name: data.Name, + } + + if err := json.Unmarshal(data.Template, &template.Template); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + template.Template = datatypes.JSON(data.Template) + + user := utils.GetXUser(r) + template.LastUpdatedBy = user.CID + + if err := template.Create(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + render.Status(r, http.StatusCreated) + utils.Render(w, r, newOTSTemplateResponse(template)) +} + +// listTemplates godoc +// @Summary List all OTS templates +// @Description List all OTS templates +// @Tags training +// @Accept json +// @Produce json +// @Success 200 {object} []otsTemplateResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates [get] +func listTemplates(w http.ResponseWriter, r *http.Request) { + templates, err := models.GetAllOTSTemplates() + if err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + if err := render.RenderList(w, r, newTemplateList(templates)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// patchTemplate godoc +// @Summary Patch an OTS template +// @Description Patch an OTS template +// @Tags training +// @Accept json +// @Produce json +// @Param id path int true "Template ID" +// @Param template body otsTemplateRequest true "Template" +// @Success 200 {object} otsTemplateResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates/{id} [patch] +func patchTemplate(w http.ResponseWriter, r *http.Request) { + template := utils.GetOTSTemplateCtx(r) + data := &otsTemplateRequest{} + if err := data.Bind(r); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + if data.Name != "" { + template.Name = data.Name + } + if len(data.Template) != 0 { + // Try to unmarshal the template + if err := json.Unmarshal(data.Template, &template.Template); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + template.Template = datatypes.JSON(data.Template) + } + + user := utils.GetXUser(r) + template.LastUpdatedBy = user.CID + + if err := template.Update(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + utils.Render(w, r, newOTSTemplateResponse(template)) +} + +// deleteTemplate godoc +// @Summary Delete an OTS template +// @Description Delete an OTS template +// @Tags training +// @Accept json +// @Produce json +// @Param id path int true "Template ID" +// @Success 204 +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates/{id} [delete] +func deleteTemplate(w http.ResponseWriter, r *http.Request) { + template := utils.GetOTSTemplateCtx(r) + if err := template.Delete(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + render.Status(r, http.StatusNoContent) +} diff --git a/views/v3/training/router.go b/views/v3/training/router.go new file mode 100644 index 0000000..28446a4 --- /dev/null +++ b/views/v3/training/router.go @@ -0,0 +1,53 @@ +package training + +import ( + "context" + "github.com/VATUSA/primary-api/pkg/database/models" + middleware "github.com/VATUSA/primary-api/pkg/go-chi/middleware/auth" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/chi/v5" + "net/http" + "strconv" +) + +func Router(r chi.Router) { + r.Use(middleware.NotGuest) + + r.Route("/ots", func(r chi.Router) { + r.Route("/templates", func(r chi.Router) { + r.Get("/", listTemplates) + r.With(middleware.CanEditOTSTemplate).Post("/", createTemplate) + r.Route("/{TemplateID}", func(r chi.Router) { + r.Use(templateCtx) + + r.With(middleware.CanEditOTSTemplate).Patch("/", patchTemplate) + r.With(middleware.CanEditOTSTemplate).Delete("/", deleteTemplate) + }) + }) + }) +} + +func templateCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "TemplateID") + if id == "" { + http.Error(w, "Invalid otsTemplateRequest", http.StatusBadRequest) + return + } + + TemplateID, err := strconv.ParseUint(id, 10, 64) + if err != nil { + http.Error(w, "Invalid otsTemplateRequest", http.StatusBadRequest) + return + } + + template := &models.OTSTemplate{ID: uint(TemplateID)} + if err = template.Get(); err != nil { + http.Error(w, "Invalid otsTemplateRequest", http.StatusBadRequest) + return + } + + ctx := context.WithValue(r.Context(), utils.OTSTemplateKey{}, template) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/views/v3/user/login.go b/views/v3/user/login.go index e42f1bc..b451fc5 100644 --- a/views/v3/user/login.go +++ b/views/v3/user/login.go @@ -25,6 +25,7 @@ import ( func GetLogin(w http.ResponseWriter, r *http.Request) { state, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 64) if err != nil { + log.WithError(err).Error("Error generating random state") utils.Render(w, r, utils.ErrInternalServer) return } @@ -34,6 +35,7 @@ func GetLogin(w http.ResponseWriter, r *http.Request) { "redirect": r.URL.Query().Get("redirect"), }) if err != nil { + log.WithError(err).Error("Error creating session") utils.Render(w, r, utils.ErrInternalServer) return } diff --git a/views/v3/user/router.go b/views/v3/user/router.go index 62dc247..d365b00 100644 --- a/views/v3/user/router.go +++ b/views/v3/user/router.go @@ -11,6 +11,7 @@ import ( "github.com/VATUSA/primary-api/views/v3/notification" rating_change "github.com/VATUSA/primary-api/views/v3/rating-change" "github.com/VATUSA/primary-api/views/v3/roster" + trainingrecords "github.com/VATUSA/primary-api/views/v3/training-records" user_flag "github.com/VATUSA/primary-api/views/v3/user-flag" user_notification "github.com/VATUSA/primary-api/views/v3/user-notification" user_role "github.com/VATUSA/primary-api/views/v3/user-role" @@ -62,6 +63,10 @@ func Router(r chi.Router) { r.With(middleware.NotGuest, middleware.CanViewUser).Get("/roster", roster.GetUserRosters) + r.Route("/training", func(r chi.Router) { + trainingrecords.Router(r) + }) + r.Route("/user-flag", func(r chi.Router) { user_flag.Router(r) })