diff --git a/backend/src/models/meeting.go b/backend/src/models/meeting.go index 11cc9c3a..a3dc3f2a 100644 --- a/backend/src/models/meeting.go +++ b/backend/src/models/meeting.go @@ -1,11 +1,76 @@ package models import ( + "errors" "time" "go.mongodb.org/mongo-driver/bson/primitive" ) +type MeetingKind string +type MeetingParticipantKind string + +const ( + EventMeeting MeetingKind = "EVENT" + TeamMeeting MeetingKind = "TEAM" + CompanyMeeting MeetingKind = "COMPANY" +) + +const ( + MemberParticipant MeetingParticipantKind = "MEMBER" + CompanyRepParticipant MeetingParticipantKind = "COMPANYREP" +) + +func (mk *MeetingKind) Parse(kind string) error { + + var newMeetingKind MeetingKind + + switch kind { + + case string(EventMeeting): + newMeetingKind = EventMeeting + break + + case string(TeamMeeting): + newMeetingKind = TeamMeeting + break + + case string(CompanyMeeting): + newMeetingKind = CompanyMeeting + break + + default: + return errors.New("invalid kind") + + } + + *mk = newMeetingKind + return nil +} + +func (mk *MeetingParticipantKind) Parse(participantKind string) error { + + var newMeetingParticipantKind MeetingParticipantKind + + switch participantKind { + + case string(MemberParticipant): + newMeetingParticipantKind = MemberParticipant + break + + case string(CompanyRepParticipant): + newMeetingParticipantKind = CompanyRepParticipant + break + + default: + return errors.New("invalid type of participant") + + } + + *mk = newMeetingParticipantKind + return nil +} + type MeetingParticipants struct { // Members is an array of _id of Member (see models.Member). @@ -22,6 +87,12 @@ type Meeting struct { // Meeting's ID (_id of mongodb). ID primitive.ObjectID `json:"id" bson:"_id"` + // The title of the meeting + Title string `json:"title" bson:"title"` + + // Type of the meeting + Kind MeetingKind `json:"kind" bson:"kind"` + Begin time.Time `json:"begin" bson:"begin"` End time.Time `json:"end" bson:"end"` @@ -33,5 +104,8 @@ type Meeting struct { // "Ata" in portuguese. Minute string `json:"minute" bson:"minute"` + // Communications is an array of _id of Communication (see models.Communication). + Communications []primitive.ObjectID `json:"communications" bson:"communications"` + Participants MeetingParticipants `json:"participants" bson:"participants"` } diff --git a/backend/src/models/notification.go b/backend/src/models/notification.go index d199a418..cf0d0880 100644 --- a/backend/src/models/notification.go +++ b/backend/src/models/notification.go @@ -18,7 +18,8 @@ const ( NotificationKindUpdatedPrivateImage NotificationKind = "UPDATED_PRIVATE_IMAGE" NotificationKindUpdatedPublicImage NotificationKind = "UPDATED_PUBLIC_IMAGE" - NotificationKindUploadedMeetingMinute NotificationKind = "UPDLOADED_MEETING_MINUTE" + NotificationKindUploadedMeetingMinute NotificationKind = "UPLOADED_MEETING_MINUTE" + NotificationKindDeletedMeetingMinute NotificationKind = "DELETED_MEETING_MINUTE" // Speaker's company image NotificationKindUpdatedCompanyImage NotificationKind = "UPDATED_COMPANY_IMAGE" diff --git a/backend/src/mongodb/company.go b/backend/src/mongodb/company.go index bbda4c5e..afe6e6e7 100644 --- a/backend/src/mongodb/company.go +++ b/backend/src/mongodb/company.go @@ -158,25 +158,25 @@ func (c *CompaniesType) GetCompanies(compOptions GetCompaniesOptions) ([]*models case string(NumberParticipations): query := mongo.Pipeline{ { - {"$match", filter}, + {Key: "$match", Value: filter}, }, { - {"$addFields", bson.D{ - {"numParticipations", bson.D{ - {"$size", "$participations"}, + {Key: "$addFields", Value: bson.D{ + {Key: "numParticipations", Value: bson.D{ + {Key: "$size", Value: "$participations"}, }}, }}, }, { - {"$sort", bson.D{ - {"numParticipations", -1}, + {Key: "$sort", Value: bson.D{ + {Key: "numParticipations", Value: -1}, }}, }, { - {"$skip", (*compOptions.NumRequests * (*compOptions.MaxCompInRequest))}, + {Key: "$skip", Value: (*compOptions.NumRequests * (*compOptions.MaxCompInRequest))}, }, { - {"$limit", *compOptions.MaxCompInRequest}, + {Key: "$limit", Value: *compOptions.MaxCompInRequest}, }, } cur, err = c.Collection.Aggregate(ctx, query) @@ -187,18 +187,18 @@ func (c *CompaniesType) GetCompanies(compOptions GetCompaniesOptions) ([]*models case string(LastParticipation): query := mongo.Pipeline{ { - {"$match", filter}, + {Key: "$match", Value: filter}, }, { - {"$sort", bson.D{ - {"participations.event", -1}, + {Key: "$sort", Value: bson.D{ + {Key: "participations.event", Value: -1}, }}, }, { - {"$skip", (*compOptions.NumRequests * (*compOptions.MaxCompInRequest))}, + {Key: "$skip", Value: (*compOptions.NumRequests * (*compOptions.MaxCompInRequest))}, }, { - {"$limit", *compOptions.MaxCompInRequest}, + {Key: "$limit", Value: *compOptions.MaxCompInRequest}, }, } cur, err = c.Collection.Aggregate(ctx, query) diff --git a/backend/src/mongodb/meeting.go b/backend/src/mongodb/meeting.go index a8cb4d4e..f07a8f4f 100644 --- a/backend/src/mongodb/meeting.go +++ b/backend/src/mongodb/meeting.go @@ -24,6 +24,8 @@ type MeetingsType struct { // CreateMeetingData contains data needed to create a new meeting type CreateMeetingData struct { + Title *string `json:"title" bson:"title"` + Kind *string `json:"kind" bson:"kind"` Begin *time.Time `json:"begin"` End *time.Time `json:"end"` Place *string `json:"place"` @@ -39,11 +41,19 @@ type GetMeetingsOptions struct { // UpdateMeetingData contains data needed to update a new meeting type UpdateMeetingData struct { + Title string `json:"title" bson:"title"` + Kind string `json:"kind" bson:"kind"` Begin time.Time `json:"begin" bson:"begin"` End time.Time `json:"end" bson:"end"` Place string `json:"place" bson:"place"` } +// MeetingParticipantData contains data needed to add a meeting participant +type MeetingParticipantData struct { + Member primitive.ObjectID `json:"memberID"` + Type string `json:"type"` +} + //ParseBody fills an UpdateMeetingData from a body func (umd *UpdateMeetingData) ParseBody(body io.Reader) error { ctx = context.Background() @@ -54,6 +64,13 @@ func (umd *UpdateMeetingData) ParseBody(body io.Reader) error { if len(umd.Place) == 0 { return errors.New("invalid place") } + if len(umd.Title) == 0 { + return errors.New("invalid title") + } + var mk = new(models.MeetingKind) + if err := mk.Parse(umd.Kind); err != nil { + return errors.New("invalid kind") + } if umd.Begin.After(umd.End) { return errors.New("invalid begin and end dates: begin must be before end") @@ -82,6 +99,46 @@ func (cmd *CreateMeetingData) Validate() error { return errors.New("no place given") } + if cmd.Title == nil { + return errors.New("no title given") + } + + var mk = new(models.MeetingKind) + if err := mk.Parse(*cmd.Kind); err != nil { + return errors.New("invalid kind") + } + + return nil +} + +// ParseBody fills the CreateMeetingParticipantData from a body +func (cmd *MeetingParticipantData) ParseBody(body io.Reader) error { + ctx = context.Background() + + if err := json.NewDecoder(body).Decode(cmd); err != nil { + return err + } + + if err := cmd.Validate(); err != nil { + return err + } + + return nil +} + +// Validate the data for adding a meeting participant +func (cmd *MeetingParticipantData) Validate() error { + ctx = context.Background() + + if len(cmd.Member) == 0 { + return errors.New("no participant given") + } + + var mk = new(models.MeetingParticipantKind) + if err := mk.Parse(cmd.Type); err != nil { + return errors.New("invalid type of participant") + } + return nil } @@ -107,9 +164,13 @@ func (m *MeetingsType) CreateMeeting(data CreateMeetingData) (*models.Meeting, e var meeting models.Meeting var c = bson.M{ - "begin": *data.Begin, - "end": *data.End, - "place": *data.Place, + "title": *data.Title, + "kind": *data.Kind, + "begin": *data.Begin, + "end": *data.End, + "place": *data.Place, + "communications": []primitive.ObjectID{}, + "participants": models.MeetingParticipants{Members: []primitive.ObjectID{}, CompanyReps: []primitive.ObjectID{}}, } if data.Participants != nil { @@ -263,6 +324,8 @@ func (m *MeetingsType) UpdateMeeting(data UpdateMeetingData, meetingID primitive var updateQuery = bson.M{ "$set": bson.M{ + "title": data.Title, + "kind": data.Kind, "begin": data.Begin, "end": data.End, "place": data.Place, @@ -305,3 +368,140 @@ func (m *MeetingsType) UploadMeetingMinute(meetingID primitive.ObjectID, url str return &updatedMeeting, nil } + +func (m *MeetingsType) DeleteMeetingMinute(meetingID primitive.ObjectID) (*models.Meeting, error) { + var updateQuery = bson.M{ + "$set": bson.M{ + "minute": "", + }, + } + + var filterQuery = bson.M{"_id": meetingID} + + var optionsQuery = options.FindOneAndUpdate() + optionsQuery.SetReturnDocument(options.After) + + var updatedMeeting models.Meeting + + if err := m.Collection.FindOneAndUpdate(ctx, filterQuery, updateQuery, optionsQuery).Decode(&updatedMeeting); err != nil { + log.Println("error updating meeting:", err) + return nil, err + } + + return &updatedMeeting, nil +} + +func (m *MeetingsType) AddMeetingParticipant(meetingID primitive.ObjectID, data MeetingParticipantData) (*models.Meeting, error) { + var updateQuery primitive.M + if data.Type == "MEMBER" { + updateQuery = bson.M{ + "$addToSet": bson.M{ + "participants.members": data.Member, + }, + } + } else if data.Type == "COMPANYREP" { + updateQuery = bson.M{ + "$addToSet": bson.M{ + "participants.companyReps": data.Member, + }, + } + } else { + return nil, errors.New("Invalid type of participant") + } + + var filterQuery = bson.M{"_id": meetingID} + + var optionsQuery = options.FindOneAndUpdate() + optionsQuery.SetReturnDocument(options.After) + + var updatedMeeting models.Meeting + + if err := m.Collection.FindOneAndUpdate(ctx, filterQuery, updateQuery, optionsQuery).Decode(&updatedMeeting); err != nil { + log.Println("error updating meeting:", err) + return nil, err + } + + return &updatedMeeting, nil +} + +func (m *MeetingsType) DeleteMeetingParticipant(meetingID primitive.ObjectID, data MeetingParticipantData) (*models.Meeting, error) { + var updateQuery primitive.M + if data.Type == "MEMBER" { + updateQuery = bson.M{ + "$pull": bson.M{ + "participants.members": data.Member, + }, + } + } else if data.Type == "COMPANYREP" { + updateQuery = bson.M{ + "$pull": bson.M{ + "participants.companyReps": data.Member, + }, + } + } else { + return nil, errors.New("Invalid type of participant") + } + + var filterQuery = bson.M{"_id": meetingID} + + var optionsQuery = options.FindOneAndUpdate() + optionsQuery.SetReturnDocument(options.After) + + var updatedMeeting models.Meeting + + if err := m.Collection.FindOneAndUpdate(ctx, filterQuery, updateQuery, optionsQuery).Decode(&updatedMeeting); err != nil { + log.Println("error updating meeting:", err) + return nil, err + } + + return &updatedMeeting, nil +} + +// AddThread adds a models.Thread to a meeting's list of communications. +func (m *MeetingsType) AddThread(meetingID primitive.ObjectID, threadID primitive.ObjectID) (*models.Meeting, error) { + ctx := context.Background() + + var updatedMeeting models.Meeting + + var updateQuery = bson.M{ + "$addToSet": bson.M{ + "communications": threadID, + }, + } + + var filterQuery = bson.M{"_id": meetingID} + + var optionsQuery = options.FindOneAndUpdate() + optionsQuery.SetReturnDocument(options.After) + + if err := m.Collection.FindOneAndUpdate(ctx, filterQuery, updateQuery, optionsQuery).Decode(&updatedMeeting); err != nil { + log.Println("Error adding communication to meeting:", err) + return nil, err + } + + return &updatedMeeting, nil +} + +// FindThread finds a thread in a meeting +func (m *MeetingsType) FindThread(threadID primitive.ObjectID) (*models.Meeting, error) { + ctx := context.Background() + filter := bson.M{ + "communications": threadID, + } + + cur, err := m.Collection.Find(ctx, filter) + if err != nil { + return nil, err + } + + var meeting models.Meeting + + if cur.Next(ctx) { + if err := cur.Decode(&meeting); err != nil { + return nil, err + } + return &meeting, nil + } + + return nil, nil +} diff --git a/backend/src/mongodb/member.go b/backend/src/mongodb/member.go index 7686577e..ef7fcd27 100644 --- a/backend/src/mongodb/member.go +++ b/backend/src/mongodb/member.go @@ -243,18 +243,18 @@ func (m *MembersType) GetMembers(options GetMemberOptions) ([]*models.Member, er if options.Event != nil { query = append(query, bson.D{ - {"$match", bson.M{"event._id": *options.Event}}, + {Key: "$match", Value: bson.M{"event._id": *options.Event}}, }) } query = append(query, bson.D{ - {"$group", bson.D{ - {"_id", "$_id"}, - {"name", bson.M{"$first": "$name"}}, - {"sinfoid", bson.M{"$first": "$sinfoid"}}, - {"img", bson.M{"$first": "$img"}}, - {"istid", bson.M{"$first": "$istid"}}, - {"contact", bson.M{"$first": "$contact"}}, + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$_id"}, + {Key: "name", Value: bson.M{"$first": "$name"}}, + {Key: "sinfoid", Value: bson.M{"$first": "$sinfoid"}}, + {Key: "img", Value: bson.M{"$first": "$img"}}, + {Key: "istid", Value: bson.M{"$first": "$istid"}}, + {Key: "contact", Value: bson.M{"$first": "$contact"}}, }}, }) @@ -361,7 +361,7 @@ func (m *MembersType) GetMembersParticipations(id primitive.ObjectID) ([]*models } for _, teamMember := range team.Members { if teamMember.Member == id { - memEvtTeam := models.MemberEventTeam{event.ID, team.Name, teamMember.Role} + memEvtTeam := models.MemberEventTeam{Event: event.ID, Team: team.Name, Role: teamMember.Role} memberEventTeams = append(memberEventTeams, &memEvtTeam) teamFound = true break diff --git a/backend/src/mongodb/session.go b/backend/src/mongodb/session.go index 6fc5f9f4..f1cad58d 100644 --- a/backend/src/mongodb/session.go +++ b/backend/src/mongodb/session.go @@ -79,6 +79,7 @@ type CreateSessionData struct { Description *string `json:"description"` Place *string `json:"place"` Kind *string `json:"kind"` + VideoURL *string `json:"videoURL"` Company *primitive.ObjectID `json:"company"` Speakers *[]primitive.ObjectID `json:"speaker"` Tickets *models.SessionTickets `json:"tickets"` @@ -199,6 +200,10 @@ func (s *SessionsType) CreateSession(data CreateSessionData) (*models.Session, e c["speaker"] = data.Speakers } + if data.VideoURL != nil { + c["videoURL"] = data.VideoURL + } + if data.Tickets != nil { c["tickets"] = bson.M{ "start": data.Tickets.Start.UTC(), diff --git a/backend/src/mongodb/speaker.go b/backend/src/mongodb/speaker.go index cf19ca3b..563a0d98 100644 --- a/backend/src/mongodb/speaker.go +++ b/backend/src/mongodb/speaker.go @@ -150,12 +150,12 @@ func (s *SpeakersType) CreateSpeaker(data CreateSpeakerData) (*models.Speaker, e // The field is non-existent if it has a nil value. // This filter will behave like a logical *and*. type GetSpeakersOptions struct { - EventID *int - MemberID *primitive.ObjectID - Name *string - NumRequests *int64 - MaxSpeaksInRequest *int64 - SortingMethod *string + EventID *int + MemberID *primitive.ObjectID + Name *string + NumRequests *int64 + MaxSpeaksInRequest *int64 + SortingMethod *string } // GetSpeakers gets all speakers specified with a query @@ -201,25 +201,25 @@ func (s *SpeakersType) GetSpeakers(speakOptions GetSpeakersOptions) ([]*models.S case string(NumberParticipations): query := mongo.Pipeline{ { - {"$match", filter}, + {Key: "$match", Value: filter}, }, { - {"$addFields", bson.D{ - {"numParticipations", bson.D{ - {"$size", "$participations"}, + {Key: "$addFields", Value: bson.D{ + {Key: "numParticipations", Value: bson.D{ + {Key: "$size", Value: "$participations"}, }}, }}, }, { - {"$sort", bson.D{ - {"numParticipations", -1}, + {Key: "$sort", Value: bson.D{ + {Key: "numParticipations", Value: -1}, }}, }, { - {"$skip", (*speakOptions.NumRequests * (*speakOptions.MaxSpeaksInRequest))}, + {Key: "$skip", Value: (*speakOptions.NumRequests * (*speakOptions.MaxSpeaksInRequest))}, }, { - {"$limit", *speakOptions.MaxSpeaksInRequest}, + {Key: "$limit", Value: *speakOptions.MaxSpeaksInRequest}, }, } cur, err = s.Collection.Aggregate(ctx, query) @@ -230,18 +230,18 @@ func (s *SpeakersType) GetSpeakers(speakOptions GetSpeakersOptions) ([]*models.S case string(LastParticipation): query := mongo.Pipeline{ { - {"$match", filter}, + {Key: "$match", Value: filter}, }, { - {"$sort", bson.D{ - {"participations.event", -1}, + {Key: "$sort", Value: bson.D{ + {Key: "participations.event", Value: -1}, }}, }, { - {"$skip", (*speakOptions.NumRequests * (*speakOptions.MaxSpeaksInRequest))}, + {Key: "$skip", Value: (*speakOptions.NumRequests * (*speakOptions.MaxSpeaksInRequest))}, }, { - {"$limit", *speakOptions.MaxSpeaksInRequest}, + {Key: "$limit", Value: *speakOptions.MaxSpeaksInRequest}, }, } cur, err = s.Collection.Aggregate(ctx, query) diff --git a/backend/src/router/company.go b/backend/src/router/company.go index e565d782..2a7cf2c8 100644 --- a/backend/src/router/company.go +++ b/backend/src/router/company.go @@ -326,9 +326,9 @@ func (acd *addThreadData) ParseBody(body io.Reader) error { return errors.New("invalid kind") } - if *acd.Kind == models.ThreadKindMeeting && acd.Meeting == nil { - return errors.New("thread kind is meeting and meeting data is not given") - } + // if *acd.Kind == models.ThreadKindMeeting && acd.Meeting == nil { + // return errors.New("thread kind is meeting and meeting data is not given") + // } return nil } diff --git a/backend/src/router/init.go b/backend/src/router/init.go index c793c530..5dabe08a 100644 --- a/backend/src/router/init.go +++ b/backend/src/router/init.go @@ -244,10 +244,10 @@ func InitializeRouter() { memberRouter.HandleFunc("", authCoordinator(createMember)).Methods("POST") memberRouter.HandleFunc("/{id}", authMember(getMember)).Methods("GET") memberRouter.HandleFunc("/{id}/role", authMember(getMemberRole)).Methods("GET") - //members_id_participations.json; swagger/swagger.json memberRouter.HandleFunc("/{id}/participations", authMember(getMembersParticipations)).Methods("GET") memberRouter.HandleFunc("/{id}", authAdmin(updateMember)).Methods("PUT") memberRouter.HandleFunc("/{id}", authAdmin(deleteMember)).Methods("DELETE") + memberRouter.HandleFunc("/{id}/image", authMember(setMemberImage)).Methods("POST") // item handlers itemRouter := r.PathPrefix("/items").Subrouter() @@ -278,7 +278,11 @@ func InitializeRouter() { meetingRouter.HandleFunc("/{id}", authMember(getMeeting)).Methods("GET") meetingRouter.HandleFunc("/{id}", authCoordinator(deleteMeeting)).Methods("DELETE") meetingRouter.HandleFunc("/{id}", authCoordinator(updateMeeting)).Methods("PUT") + meetingRouter.HandleFunc("/{id}/thread", authMember(addMeetingThread)).Methods("POST") meetingRouter.HandleFunc("/{id}/minute", authMember(uploadMeetingMinute)).Methods("POST") + meetingRouter.HandleFunc("/{id}/minute", authMember(deleteMeetingMinute)).Methods("DELETE") + meetingRouter.HandleFunc("/{id}/participants", authMember(addMeetingParticipant)).Methods("POST") + meetingRouter.HandleFunc("/{id}/participants", authMember(deleteMeetingParticipant)).Methods("DELETE") // threads handlers threadRouter := r.PathPrefix("/threads").Subrouter() diff --git a/backend/src/router/meetings.go b/backend/src/router/meetings.go index aeabf049..cde04ab6 100644 --- a/backend/src/router/meetings.go +++ b/backend/src/router/meetings.go @@ -83,6 +83,100 @@ func createMeeting(w http.ResponseWriter, r *http.Request) { } } +func addMeetingThread(w http.ResponseWriter, r *http.Request) { + + defer r.Body.Close() + + params := mux.Vars(r) + meetingID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Meetings.GetMeeting(meetingID); err != nil { + http.Error(w, "Invalid meeting ID", http.StatusNotFound) + return + } + + var atd = &addThreadData{} + + if err := atd.ParseBody(r.Body); err != nil { + http.Error(w, "Could not parse body", http.StatusBadRequest) + return + } + + credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials) + + if !ok { + http.Error(w, "Could not parse credentials", http.StatusBadRequest) + return + } + + // create the post first + var cpd = mongodb.CreatePostData{ + Member: credentials.ID, + Text: *atd.Text, + } + + newPost, err := mongodb.Posts.CreatePost(cpd) + + if err != nil { + http.Error(w, "Could not create post", http.StatusExpectationFailed) + return + } + + // assuming that meeting is already created + if *atd.Kind != models.ThreadKindMeeting { + http.Error(w, "Kind of thread must be meeting", http.StatusInternalServerError) + return + } + + // create the thread + var ctd = mongodb.CreateThreadData{ + Entry: newPost.ID, + Kind: *atd.Kind, + } + + newThread, err := mongodb.Threads.CreateThread(ctd) + + if err != nil { + http.Error(w, "Could not create thread", http.StatusExpectationFailed) + + // clean up the created post + if _, err := mongodb.Posts.DeletePost(newPost.ID); err != nil { + log.Printf("error deleting post: %s\n", err.Error()) + } + + return + } + + // and finally update the meeting participation with the created thread + updatedMeeting, err := mongodb.Meetings.AddThread(meetingID, newThread.ID) + + if err != nil { + http.Error(w, "Could not add thread to meeting", http.StatusExpectationFailed) + + // clean up the created post and thread + if _, err := mongodb.Posts.DeletePost(newPost.ID); err != nil { + log.Printf("error deleting post: %s\n", err.Error()) + } + + if _, err := mongodb.Threads.DeleteThread(newThread.ID); err != nil { + log.Printf("error deleting thread: %s\n", err.Error()) + } + + return + } + + json.NewEncoder(w).Encode(updatedMeeting) + + // notify + if credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials); ok { + mongodb.Notifications.Notify(credentials.ID, mongodb.CreateNotificationData{ + Kind: models.NotificationKindCreated, + Speaker: &updatedMeeting.ID, + Thread: &newThread.ID, + }) + } +} + func getMeetings(w http.ResponseWriter, r *http.Request) { urlQuery := r.URL.Query() @@ -241,3 +335,110 @@ func uploadMeetingMinute(w http.ResponseWriter, r *http.Request) { }) } } + +func deleteMeetingMinute(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + meetingID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Meetings.GetMeeting(meetingID); err != nil { + http.Error(w, "Invalid meeting ID", http.StatusNotFound) + return + } + + currentEvent, err := mongodb.Events.GetCurrentEvent() + if err != nil { + http.Error(w, "Couldn't fetch current event", http.StatusExpectationFailed) + return + } + + err = spaces.DeleteMeetingMinute(currentEvent.ID, meetingID) + if err != nil { + http.Error(w, fmt.Sprintf("Couldn't upload file: %v", err), http.StatusExpectationFailed) + return + } + + updatedMeeting, err := mongodb.Meetings.DeleteMeetingMinute(meetingID) + if err != nil { + http.Error(w, "Couldn't delete meeting minutes", http.StatusExpectationFailed) + return + } + + json.NewEncoder(w).Encode(updatedMeeting) + + // notify + if credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials); ok { + mongodb.Notifications.Notify(credentials.ID, mongodb.CreateNotificationData{ + Kind: models.NotificationKindDeletedMeetingMinute, + Meeting: &updatedMeeting.ID, + }) + } +} + +func addMeetingParticipant(w http.ResponseWriter, r *http.Request) { + + defer r.Body.Close() + + params := mux.Vars(r) + meetingID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Meetings.GetMeeting(meetingID); err != nil { + http.Error(w, "Invalid meeting ID", http.StatusNotFound) + return + } + + var cmd = mongodb.MeetingParticipantData{} + + if err := cmd.ParseBody(r.Body); err != nil { + http.Error(w, "Could not parse body: "+err.Error(), http.StatusBadRequest) + return + } + + meeting, err := mongodb.Meetings.AddMeetingParticipant(meetingID, cmd) + if err != nil { + http.Error(w, "Could not add participant to meeting", http.StatusExpectationFailed) + } + + json.NewEncoder(w).Encode(meeting) + + // notify + if credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials); ok { + mongodb.Notifications.Notify(credentials.ID, mongodb.CreateNotificationData{ + Kind: models.NotificationKindCreated, + Meeting: &meeting.ID, + }) + } +} + +func deleteMeetingParticipant(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + params := mux.Vars(r) + meetingID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Meetings.GetMeeting(meetingID); err != nil { + http.Error(w, "Invalid meeting ID", http.StatusNotFound) + return + } + + var cmd = mongodb.MeetingParticipantData{} + + if err := cmd.ParseBody(r.Body); err != nil { + http.Error(w, "Could not parse body: "+err.Error(), http.StatusBadRequest) + return + } + + meeting, err := mongodb.Meetings.DeleteMeetingParticipant(meetingID, cmd) + if err != nil { + http.Error(w, "Could not add participant to meeting", http.StatusExpectationFailed) + } + + json.NewEncoder(w).Encode(meeting) + + // notify + if credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials); ok { + mongodb.Notifications.Notify(credentials.ID, mongodb.CreateNotificationData{ + Kind: models.NotificationKindCreated, + Meeting: &meeting.ID, + }) + } +} diff --git a/backend/src/router/member.go b/backend/src/router/member.go index 872031b3..db48f4d6 100644 --- a/backend/src/router/member.go +++ b/backend/src/router/member.go @@ -1,11 +1,18 @@ package router import ( + "bytes" "encoding/json" + "fmt" + "io" + "io/ioutil" "net/http" "strconv" + "github.com/h2non/filetype" + "github.com/sinfo/deck2/src/config" "github.com/sinfo/deck2/src/models" + "github.com/sinfo/deck2/src/spaces" "github.com/sinfo/deck2/src/mongodb" "go.mongodb.org/mongo-driver/bson/primitive" @@ -80,10 +87,16 @@ func getMember(w http.ResponseWriter, r *http.Request) { func updateMember(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - var umd = &mongodb.UpdateMemberData{} params := mux.Vars(r) id, _ := primitive.ObjectIDFromHex(params["id"]) + if _, err := mongodb.Members.GetMember(id); err != nil { + http.Error(w, "Invalid member ID", http.StatusNotFound) + return + } + + var umd = &mongodb.UpdateMemberData{} + if err := umd.ParseBody(r.Body); err != nil { http.Error(w, "Could not parse body", http.StatusBadRequest) return @@ -99,6 +112,78 @@ func updateMember(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(updatedMember) } +func setMemberImage(w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + memberID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Members.GetMember(memberID); err != nil { + http.Error(w, "Invalid member ID", http.StatusNotFound) + return + } + + if err := r.ParseMultipartForm(config.ImageMaxSize); err != nil { + http.Error(w, fmt.Sprintf("Exceeded file size (%v bytes)", config.ImageMaxSize), http.StatusBadRequest) + return + } + + file, handler, err := r.FormFile("image") + if err != nil { + http.Error(w, "Invalid payload", http.StatusBadRequest) + return + } + + // check again for file size + // the previous check fails only if a chunk > maxSize is sent, but this tests the whole file + if handler.Size > config.ImageMaxSize { + http.Error(w, fmt.Sprintf("Exceeded file size (%v bytes)", config.ImageMaxSize), http.StatusBadRequest) + return + } + + defer file.Close() + + currentEvent, err := mongodb.Events.GetCurrentEvent() + if err != nil { + http.Error(w, "Couldn't fetch current event", http.StatusExpectationFailed) + return + } + + // must duplicate the reader so that we can get some information first, and then pass it to the spaces package + var buf bytes.Buffer + checker := io.TeeReader(file, &buf) + + bytes, err := ioutil.ReadAll(checker) + if err != nil { + http.Error(w, "Unable to read the file", http.StatusExpectationFailed) + return + } + + if !filetype.IsImage(bytes) { + http.Error(w, "Not an image", http.StatusBadRequest) + return + } + + kind, err := filetype.Match(bytes) + if err != nil { + http.Error(w, "Unable to get file type", http.StatusExpectationFailed) + return + } + + url, err := spaces.UploadMemberImage(currentEvent.ID, memberID, &buf, handler.Size, kind.MIME.Value) + if err != nil { + http.Error(w, fmt.Sprintf("Couldn't upload file: %v", err), http.StatusExpectationFailed) + return + } + + updatedMember, err := mongodb.Members.UpdateImage(memberID, *url) + if err != nil { + http.Error(w, "Couldn't update member internal image", http.StatusExpectationFailed) + return + } + + json.NewEncoder(w).Encode(updatedMember) +} + func deleteMember(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/backend/src/router/thread.go b/backend/src/router/thread.go index 5b94b445..c49e8082 100644 --- a/backend/src/router/thread.go +++ b/backend/src/router/thread.go @@ -104,21 +104,31 @@ func addCommentToThread(w http.ResponseWriter, r *http.Request) { if err != nil { log.Println("error finding thread: " + err.Error()) return + } else if speaker != nil { + cnd.Speaker = &speaker.ID + mongodb.Notifications.Notify(credentials.ID, cnd) + return } - if speaker == nil { - company, err := mongodb.Companies.FindThread(threadID) - if err != nil { - log.Println("error finding thread: " + err.Error()) - return - } - if company != nil { - cnd.Company = &company.ID - } - } else { - cnd.Speaker = &speaker.ID + company, err := mongodb.Companies.FindThread(threadID) + if err != nil { + log.Println("error finding thread: " + err.Error()) + return + } else if company != nil { + cnd.Company = &company.ID + mongodb.Notifications.Notify(credentials.ID, cnd) + return + } + + meeting, err := mongodb.Meetings.FindThread(threadID) + if err != nil { + log.Println("error finding thread: " + err.Error()) + return + } else if meeting != nil { + cnd.Meeting = &meeting.ID + mongodb.Notifications.Notify(credentials.ID, cnd) + return } - mongodb.Notifications.Notify(credentials.ID, cnd) } func removeCommentFromThread(w http.ResponseWriter, r *http.Request) { diff --git a/backend/src/spaces/init.go b/backend/src/spaces/init.go index 973aa9ea..69e947eb 100644 --- a/backend/src/spaces/init.go +++ b/backend/src/spaces/init.go @@ -138,3 +138,16 @@ func uploadImage(path string, reader io.Reader, objectSize int64, MIME string) ( return &url, nil } + +func deleteObject(path string) error { + + path = fmt.Sprintf("%s/%s", basePath, path) + + err := client.RemoveObject(name, path) + + if err != nil { + return err + } + + return nil +} diff --git a/backend/src/spaces/meeting.go b/backend/src/spaces/meeting.go index 074cffdf..27063c5f 100644 --- a/backend/src/spaces/meeting.go +++ b/backend/src/spaces/meeting.go @@ -15,3 +15,8 @@ func UploadMeetingMinute(event int, meeting primitive.ObjectID, reader io.Reader path := fmt.Sprintf("sinfo-%d/%s/%s", event, meetingPath, meeting.Hex()) return uploadImage(path, reader, objectSize, MIME) } + +func DeleteMeetingMinute(event int, meeting primitive.ObjectID) error { + path := fmt.Sprintf("sinfo-%d/%s/%s", event, meetingPath, meeting.Hex()) + return deleteObject(path) +} diff --git a/backend/static/swagger.json b/backend/static/swagger.json index a94480a8..3db72eff 100644 --- a/backend/static/swagger.json +++ b/backend/static/swagger.json @@ -1 +1 @@ -{"schemes":["http"],"swagger":"2.0","info":{"description":"SINFO's internal management application\n","title":"Swagger Deck2","termsOfService":"http://swagger.io/terms/","contact":{"email":"devteam@sinfo.org"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.0"},"host":"localhost:8080","basePath":"/","paths":{"/auth/callback":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["auth"],"summary":"Authentication callback endpoint","operationId":"authCallback","responses":{"317":{"description":"Redirects to deck2 website with authentication code"},"401":{"description":"Unauthorized (reason in the body of the response)"}}}},"/auth/login":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["auth"],"summary":"Authenticates member","operationId":"authLogin","parameters":[{"type":"string","description":"URl to be redirected after the successful login\n\n\u003cb\u003eExample:\u003c/b\u003e \n \u003ccode\u003eGET /auth/login?redirect=example.com/some/path\u003c/code\u003e \n On a successful login, you'll be redirected to \u003ccode\u003eexample.com/some/path/{generated_authentication_token}\u003c/code\u003e","name":"redirect","in":"query"}],"responses":{"317":{"description":"Redirects to authentication server"}}}},"/auth/verify/{token}":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["auth"],"summary":"Authentication verification endpoint","operationId":"verifyToken","parameters":[{"type":"string","description":"JWT token sent to be verified","name":"token","in":"path","required":true}],"responses":{"200":{"description":"Token is valid"},"401":{"description":"Token in invalid, therefore unauthorized"}}}},"/billings":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Get all billings. If role is lower than coordinator, only visible billings will be returned","operationId":"getBillings","parameters":[{"type":"string","format":"date-time","description":"Billings emited after date","name":"after","in":"query"},{"type":"string","format":"date-time","description":"Billings emited before date","name":"before","in":"query"},{"type":"string","description":"Billings with value greater than this value","name":"valueGreaterThan","in":"query"},{"type":"string","description":"Billings with value lower than this value","name":"valueLessThan","in":"query"},{"type":"string","description":"Billings from this event","name":"event","in":"query"},{"type":"string","description":"Billings from this company","name":"company","in":"query"}],"responses":{"200":{"description":"Billings filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/billing"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get billings"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Creates a new billing","operationId":"createBilling","parameters":[{"description":"Information needed to create the new company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["status","event","value","invoiceNumber","emission","notes"],"properties":{"emission":{"type":"string","format":"date-time"},"event":{"type":"integer"},"invoiceNumber":{"type":"string"},"notes":{"type":"string"},"status":{"type":"object","required":["proForma","paid","receipt","invoice"],"properties":{"invoice":{"type":"boolean"},"paid":{"type":"boolean"},"proForma":{"type":"boolean"},"receipt":{"type":"boolean"}}},"value":{"type":"integer"}}}}],"responses":{"200":{"description":"Created billing","schema":{"$ref":"#/definitions/billing"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Couldn't find created billing"}}}},"/billings/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Gets a billing","operationId":"getBilling","parameters":[{"type":"string","description":"ID of billing","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Specified Billing","schema":{"$ref":"#/definitions/billing"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Billing not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Updates a billing","operationId":"updateBilling","parameters":[{"type":"string","description":"ID of billing","name":"id","in":"path","required":true},{"description":"Information needed to create the new company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["status","event","value","invoiceNumber","emission","notes"],"properties":{"emission":{"type":"string","format":"date-time"},"event":{"type":"integer"},"invoiceNumber":{"type":"string"},"notes":{"type":"string"},"status":{"type":"object","required":["proForma","paid","receipt","invoice"],"properties":{"invoice":{"type":"boolean"},"paid":{"type":"boolean"},"proForma":{"type":"boolean"},"receipt":{"type":"boolean"}}},"value":{"type":"integer"}}}}],"responses":{"200":{"description":"Updated billing","schema":{"$ref":"#/definitions/billing"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Couldn't find specified billing"}}}},"/companies":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Get all companies","operationId":"getCompanies","parameters":[{"type":"string","description":"Name of the company","name":"name","in":"query"},{"type":"integer","description":"Has a participation entry for this event","name":"event","in":"query"},{"type":"string","description":"Was contacted by this member","name":"member","in":"query"},{"type":"boolean","description":"Participated as a partner","name":"partner","in":"query"}],"responses":{"200":{"description":"Companies filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/company"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get companies"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Create a new company","operationId":"createCompany","parameters":[{"description":"Information needed to create the new company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","description","site"],"properties":{"description":{"type":"string"},"name":{"type":"string"},"site":{"type":"string"}}}}],"responses":{"200":{"description":"Created company.","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid input or couldn't create the new company"},"401":{"description":"Unauthorized"}}}},"/companies/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Get company by ID","operationId":"getCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Company with the specific ID","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Update company by ID","operationId":"updateCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"Information needed to update the company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","description","site"],"properties":{"billingInfo":{"type":"object","properties":{"address":{"type":"string"},"name":{"type":"string"},"tin":{"type":"string"}}},"description":{"type":"string"},"name":{"type":"string"},"site":{"type":"string"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to update company"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Delete company by ID (must have admin credentials)","operationId":"deleteCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Unable to delete company"}}}},"/companies/{id}/employer":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","companyReps","contacts"],"summary":"Creates a new companyRep and adds it to a company","operationId":"addEmployer","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"Information needed to create the new companyRep.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"contact":{"type":"object","required":["phones","socials","mails"],"properties":{"mails":{"type":"array","items":{"type":"object","required":["mail","valid","personal"],"properties":{"personal":{"type":"boolean"},"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","required":["phone","valid"],"properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","required":["facebook","skype","github","twitter","linkedin"],"properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Couldn't find company or created company rep or created contact"}}}},"/companies/{id}/employer/{rep}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","companyReps"],"summary":"Deletes a companyRep and removes it from company ","operationId":"removeEmployer","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"string","description":"ID of the companyRep","name":"rep","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Couldn't find company or company rep"}}}},"/companies/{id}/image/internal":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["companies"],"summary":"Update company's internal image by ID","operationId":"updateCompanyInternalImage","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"file","description":"Logo of company","name":"image","in":"formData"}],"responses":{"200":{"description":"Company with the updated data","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to perform operation"}}}},"/companies/{id}/image/public":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["companies"],"summary":"Update company's public image by ID (must have coordination credentials)","operationId":"updateCompanyPublicImage","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"file","description":"Logo of company","name":"image","in":"formData"}],"responses":{"200":{"description":"Company with the updated data","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Company not found"},"417":{"description":"Unable to perform operation"}}}},"/companies/{id}/participation":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Updates participation data on the current event to a company","operationId":"updateCompanyParticipation","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","partner","confirmed","notes"],"properties":{"confirmed":{"type":"string","format":"date-time"},"member":{"type":"string"},"notes":{"type":"string"},"partner":{"type":"boolean"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Unable to add participation for the current event to this company"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Adds participation on the current event to a company","operationId":"addCompanyParticipation","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["partner"],"properties":{"partner":{"type":"boolean"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}}},"/companies/{id}/participation/package":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","packages"],"summary":"Adds a package on the current event to a company (must have at least coordination credentails)","operationId":"addCompanyPackage","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","items","price","vat"],"properties":{"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"public":{"type":"boolean"},"quantity":{"type":"integer"}}}},"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Company not found"},"417":{"description":"Couldn't add package to company"}}}},"/companies/{id}/participation/status/next":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Gets all the valid steps to be taken on a company's participation status on the current event","operationId":"getvalidCompanyParticipationStatusSteps","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Valid steps","schema":{"type":"object","properties":{"steps":{"type":"array","items":{"type":"object","properties":{"next":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"step":{"type":"integer"}}}}}}},"400":{"description":"Company without participation on the current event"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}}},"/companies/{id}/participation/status/{status}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Updated a company's participation status on the current event (admin credentials)","operationId":"updateCompanyParticipationStatus","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"],"type":"string","description":"New status","name":"status","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Company not found"},"417":{"description":"Unable to update company's participation status"}}}},"/companies/{id}/participation/status/{step}":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Step a company's participation status on the current event","operationId":"stepCompanyParticipationStatus","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"integer","description":"Step to the next status","name":"step","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to update company's participation status"}}}},"/companies/{id}/subscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Subscribe to company by ID","operationId":"subscribeToCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to subscribe to company"}}}},"/companies/{id}/thread":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","threads"],"summary":"Adds thread on the current event to a company","operationId":"addCompanyThread","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New thread information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text","kind"],"properties":{"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"meeting":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"participants":{"type":"object","properties":{"companyReps":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"string"}}}},"place":{"type":"string"}}},"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid payload, or invalid credentials"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to create post, create thread or add created thread to company participation"}}}},"/companies/{id}/unsubscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Unsubscribe to company by ID","operationId":"unsubscribeToCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to unsubscribe from company"}}}},"/companyReps":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companyReps"],"summary":"Get all companyReps, based on query","operationId":"getCompanyReps","parameters":[{"type":"string","description":"Name of the companyRep","name":"name","in":"query"}],"responses":{"200":{"description":"CompanyReps filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/companyRep"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get companyReps"}}}},"/companyReps/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companyReps"],"summary":"Gets a companyRep","operationId":"getCompanyRep","parameters":[{"type":"string","description":"ID of the companyRep","name":"id","in":"path","required":true}],"responses":{"200":{"description":"CompanyRep with specified ID","schema":{"$ref":"#/definitions/companyRep"}},"401":{"description":"Unauthorized"},"404":{"description":"CompanyRep not found"},"417":{"description":"Unable to get companyRep"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companyReps"],"summary":"Updates a companyRep","operationId":"updateCompanyRep","parameters":[{"type":"string","description":"ID of the companyrep","name":"id","in":"path","required":true},{"description":"Information needed to create the new companyRep.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"contact":{"type":"object","required":["phones","socials","mails"],"properties":{"mails":{"type":"array","items":{"type":"object","required":["mail","valid","personal"],"properties":{"personal":{"type":"boolean"},"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","required":["phone","valid"],"properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","required":["facebook","skype","github","twitter","linkedin"],"properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated companyRep ","schema":{"$ref":"#/definitions/companyRep"}},"401":{"description":"Unauthorized"},"404":{"description":"CompanyRep not found"},"417":{"description":"Unable to get companies"}}}},"/contacts":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["contacts"],"summary":"Get all contacts","operationId":"getContacts","parameters":[{"type":"string","description":"Partial and case insensitive match for phone","name":"phone","in":"query"},{"type":"string","description":"Partial and case insensitive match for mail","name":"mail","in":"query"}],"responses":{"200":{"description":"Contacts filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/contact"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get contacts"}}}},"/contacts/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["contacts"],"summary":"Get contact by ID","operationId":"getContact","parameters":[{"type":"string","description":"ID of the contact","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Contact with the specific ID","schema":{"$ref":"#/definitions/contact"}},"401":{"description":"Unauthorized"},"404":{"description":"contact not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["contacts"],"summary":"Updates a contact","operationId":"updateContact","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true},{"description":"New contact data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["phones","socials","mails"],"properties":{"mails":{"type":"array","items":{"type":"object","required":["mail","valid","personal"],"properties":{"personal":{"type":"boolean"},"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","required":["phone","valid"],"properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","required":["facebook","skype","github","twitter","linkedin"],"properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}}}],"responses":{"200":{"description":"Updated contact.","schema":{"$ref":"#/definitions/contact"}},"400":{"description":"Invalid input or couldn't updated the contact."},"401":{"description":"Unauthorized"}}}},"/events":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Get all events","operationId":"getEvents","parameters":[{"type":"string","description":"Name of the event","name":"name","in":"query"},{"type":"string","format":"date-time","description":"Event happened before this date","name":"before","in":"query"},{"type":"string","format":"date-time","description":"Event happened after this date","name":"after","in":"query"},{"type":"string","format":"date-time","description":"Event happened during this date","name":"during","in":"query"}],"responses":{"200":{"description":"Events filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/event"}}},"400":{"description":"Invalid date format on query"},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get events"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Update the current event (must have coordinator credentials)","operationId":"updateEvent","parameters":[{"description":"New event data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload, or couldn't update event"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Unable to update event"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Create a new event (must have admin credentials)","operationId":"createEvent","parameters":[{"description":"Information needed to create the new event.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}],"responses":{"200":{"description":"Created event. The new ID will be an increment to the current event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid input or couldn't create the new event"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/events/items":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","items"],"summary":"Adds an item to the current event (must have coordinator credentials)","operationId":"addEventItem","parameters":[{"description":"Item to store on the current event","name":"item","in":"body","required":true,"schema":{"type":"object","required":["id"],"properties":{"item":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found, or item not found"},"417":{"description":"Unable to add item"}}}},"/events/items/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","items"],"summary":"Removes item from the current event's packages (must have coordinator credentials)","operationId":"removeEventItem","parameters":[{"type":"string","description":"ID of the item","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Event with the removed item","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to remove item from event"}}}},"/events/meetings":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","meetings"],"summary":"Creates and adds a new meeting to the current event (must have coordinator credentials)","operationId":"addEventMeeting","parameters":[{"description":"Meeting to store on the current event","name":"meeting","in":"body","required":true,"schema":{"type":"object","required":["begin","end","place"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"place":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to create or add meeting"}}}},"/events/meetings/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","meetings"],"summary":"Removes and deletes a meeting from the current event (must have coordinator credentials)","operationId":"removeMeetingFromEvent","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found or meeting not found"},"417":{"description":"Unable to remove or delete meeting"}}}},"/events/packages":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","packages"],"summary":"Add template to packages of the current event and make it available (must have coordinator credentials)","operationId":"addEventPackage","parameters":[{"description":"Package (template) to store on the current event","name":"template","in":"body","required":true,"schema":{"type":"object","required":["template","public_name"],"properties":{"public_name":{"type":"string"},"template":{"type":"string"}}}}],"responses":{"200":{"description":"Event with the updated packages","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found, or package not found"},"417":{"description":"Unable to save package on event"}}}},"/events/packages/{id}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","packages"],"summary":"Modifies template to packages on the current event (must have coordinator credentials)","operationId":"updateEventPackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true},{"description":"Updated template data","name":"template","in":"body","schema":{"type":"object","required":["template","public_name"],"properties":{"available":{"type":"boolean"},"public_name":{"type":"string"}}}}],"responses":{"200":{"description":"Event with the updated packages","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found or template not found"},"417":{"description":"Unable to update template on event"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","packages"],"summary":"Removes template to packages from the current event (must have coordinator credentials)","operationId":"removeEventPackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Event with the updated packages","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to remove package from event"}}}},"/events/sessions":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","sessions"],"summary":"Creates and adds a new session to the current event (must have coordinator credentials)","operationId":"addEventSession","parameters":[{"description":"Session to store on the current event\n - kind: TALK, PRESENTATION or WORKSHOP\n - if kind=TALK, then speaker must be specified\n - if kind=PRESENTATION or kind=WORKSHOP, then company must be specified\n - dinamizers is optional\n - space is optional","name":"session","in":"body","required":true,"schema":{"type":"object","required":["begin","end","title","description","kind"],"properties":{"begin":{"type":"string","format":"date-time"},"company":{"type":"string"},"description":{"type":"string"},"dinamizers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"position":{"type":"string"}}}},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to create or add session"}}}},"/events/teams/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","teams"],"summary":"Removes (but does not delete) a team from the current event (must have admin credentials)","operationId":"removeTeamFromEvent","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found or team not found"},"417":{"description":"Unable to remove team"}}}},"/events/themes":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Update the current event's themes (must have coordinator credentials)","operationId":"updateEventThemes","parameters":[{"description":"Themes for the event. Must have the same number of elements as there are days in the duration of the event, or be empty","name":"themes","in":"body","required":true,"schema":{"type":"object","required":["themes"],"properties":{"themes":{"type":"array","items":{"type":"string"}}}}}],"responses":{"200":{"description":"Event with the updated themes","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload (be it format, or number of elements in the array)"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to update event's themes"}}}},"/events/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Get event by ID","operationId":"getEvent","parameters":[{"type":"integer","description":"ID of the event","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Event with the specific ID","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"404":{"description":"Event not found"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Deletes an event (must have admin credentials)","operationId":"deleteEvent","parameters":[{"type":"integer","description":"ID of the event","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted event","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"}}}},"/flightInfo/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["flightInfo"],"summary":"Get flight info by ID","operationId":"getFlightInfo","parameters":[{"type":"string","description":"ID of the flight info","name":"id","in":"path","required":true}],"responses":{"200":{"description":"FlightInfo with the specific ID","schema":{"$ref":"#/definitions/flightInfo"}},"401":{"description":"Unauthorized"},"404":{"description":"FlightInfo not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["flightInfo"],"summary":"Update flight info by ID","operationId":"updateFlightInfo","parameters":[{"type":"string","description":"ID of the flight info","name":"id","in":"path","required":true},{"description":"Information needed to create a flight info.\n - Inbound/outbound: airports\n - Link: URL to the flight company's flight\n - Cost: in cents (€)","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["inbound","outbound","from","to","bought","cost","notes"],"properties":{"bought":{"type":"boolean"},"cost":{"description":"In cents (€)","type":"integer"},"from":{"description":"Airport","type":"string"},"inbound":{"type":"string","format":"date-time"},"link":{"description":"URL to the flight","type":"string"},"notes":{"type":"string"},"outbound":{"type":"string","format":"date-time"},"to":{"description":"Airport","type":"string"}}}}],"responses":{"200":{"description":"FlightInfo with the specific ID","schema":{"$ref":"#/definitions/flightInfo"}},"400":{"description":"Bad payload"},"401":{"description":"Unauthorized"},"404":{"description":"FlightInfo not found"},"417":{"description":"Unable to update FlightInfo"}}}},"/items":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Get all items","operationId":"getItems","parameters":[{"type":"string","description":"Name of the item","name":"name","in":"query"},{"type":"string","description":"Type of the item","name":"type","in":"query"}],"responses":{"200":{"description":"Items filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/item"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get items"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Creates a new item (must have coordinator credentials)","operationId":"createItem","parameters":[{"description":"New item data","name":"item","in":"body","required":true,"schema":{"type":"object","required":["name","type","description","price","vat"],"properties":{"description":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer"},"type":{"type":"string"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Created item","schema":{"$ref":"#/definitions/item"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Unable to create the item"}}}},"/items/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Get item by ID","operationId":"getItem","parameters":[{"type":"string","description":"ID of the item","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Item with the specific ID","schema":{"$ref":"#/definitions/item"}},"401":{"description":"Unauthorized"},"404":{"description":"Item not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Updates an item (must have coordinator credentials)","operationId":"updateItem","parameters":[{"description":"Information needed to update the item.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","type","description","price","vat"],"properties":{"description":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer"},"type":{"type":"string"},"vat":{"type":"integer"}}}},{"type":"string","description":"ID of the item","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated item.","schema":{"$ref":"#/definitions/item"}},"400":{"description":"Invalid input or couldn't updated the item."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Item not found"}}}},"/items/{id}/image":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["items"],"summary":"Update item image by ID","operationId":"uploadItemImage","parameters":[{"type":"string","description":"ID of the item","name":"id","in":"path","required":true},{"type":"file","description":"Photo of item","name":"image","in":"formData"}],"responses":{"200":{"description":"Item with the updated data","schema":{"$ref":"#/definitions/item"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Item not found"},"417":{"description":"Unable to perform operation"}}}},"/me":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["me"],"summary":"Get my information","operationId":"getMe","responses":{"200":{"description":"My information","schema":{"$ref":"#/definitions/member"}},"401":{"description":"Unauthorized"},"404":{"description":"Information not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["me"],"summary":"Updates my information","operationId":"updateMe","parameters":[{"description":"Information needed to update my information.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","istid"],"properties":{"istid":{"type":"string"},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated information","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid input or couldn't updated my information."},"401":{"description":"Unauthorized"}}}},"/me/image":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["me"],"summary":"Update member's image","operationId":"updateMyImage","parameters":[{"type":"file","description":"Photo of the member","name":"image","in":"formData"}],"responses":{"200":{"description":"Member with the updated data","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to perform operation"}}}},"/me/notification/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["me","notifications"],"summary":"Delete my notification","operationId":"deleteMyNotifications","parameters":[{"type":"string","description":"ID of the notification","name":"id","in":"path","required":true}],"responses":{"200":{"description":"My deleted notification","schema":{"$ref":"#/definitions/notification"}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found, or notification not found"},"417":{"description":"Unable to delete notification"}}}},"/me/notifications":{"get":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["me","notifications"],"summary":"Get member's notifications","operationId":"getMyNotifications","responses":{"200":{"description":"My notifications","schema":{"type":"array","items":{"$ref":"#/definitions/notification"}}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to get notifications"}}}},"/meetings":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Gets all meetings","operationId":"getMeetings","parameters":[{"type":"integer","format":"int64","description":"Meeting from this event","name":"event","in":"query"},{"type":"string","description":"Meeting from this team","name":"team","in":"query"},{"type":"string","description":"Meeting from this company","name":"company","in":"query"}],"responses":{"200":{"description":"Meetings filtered by query","schema":{"type":"array","items":{"$ref":"#/definitions/meeting"}}},"400":{"description":"Bad query"},"401":{"description":"Unauthorized"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Creates a meeting (must have at least coordinator credentials)","operationId":"createMeeting","parameters":[{"description":"New meeting data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["begin","end","local"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"local":{"type":"string"}}}}],"responses":{"200":{"description":"Created meeting","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Invalid payload, or couldn't create meeting"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/meetings/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Gets a meeting by id","operationId":"getMeeting","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Specified meeting","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Updates a meeting by id","operationId":"updateMeeting","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"description":"New meeting data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["begin","end","local"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"local":{"type":"string"},"minute":{"type":"string"}}}}],"responses":{"200":{"description":"Updated meeting","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Bad payload"},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Could not update meeting"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Deletes a meeting (must have at least coordinator credentials)","operationId":"deleteMeeting","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted meeting","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Meeting not found"}}}},"/meetings/{id}/minute":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["meetings"],"summary":"Upload meeting's minute by ID","operationId":"uploadMeetingMinute","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"type":"file","description":"Minute","name":"minute","in":"formData"}],"responses":{"200":{"description":"meeting with the updated data","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Invalid minute data"},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Unable to perform operation"}}}},"/members":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get all members","operationId":"getMembers","parameters":[{"type":"string","description":"Partial and case insensitive match for name","name":"name","in":"query"},{"type":"string","description":"Members from this event","name":"event","in":"query"}],"responses":{"200":{"description":"Members filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/member"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get members"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Create a new member (must have coordinator credentials)","operationId":"createMember","parameters":[{"description":"Information needed to create the new member.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","istid"],"properties":{"istid":{"type":"string"},"name":{"type":"string"},"sinfoid":{"type":"string"}}}}],"responses":{"200":{"description":"Created member.","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid input or couldn't create the new member."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/members/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get member by ID","operationId":"getMember","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Member with the specific ID","schema":{"$ref":"#/definitions/member"}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Updates a member (must have admin credentials)","operationId":"updateMember","parameters":[{"description":"Information needed to update the member.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","istid"],"properties":{"istid":{"type":"string"},"name":{"type":"string"}}}},{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated member.","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid input or couldn't updated the member."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Delete a member by ID","operationId":"deleteMember","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted Member with the specific ID","schema":{"$ref":"#/definitions/member"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Member not found"},"406":{"description":"Member associated with other objects, not possible to delete"}}}},"/members/{id}/participations":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get member's participations","operationId":"getMemberParticipations","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Member with all participations","schema":{"type":"array","items":{"$ref":"#/definitions/memberParticipation"}}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to get member's credentials"}}}},"/members/{id}/role":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get member's role","operationId":"getMemberRole","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Member with the specific ID","schema":{"type":"object","properties":{"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to get member's credentials"}}}},"/packages":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Get packages","operationId":"getPackages","parameters":[{"type":"string","description":"Name of the package","name":"name","in":"query"},{"type":"integer","description":"Price of the package","name":"price","in":"query"},{"type":"integer","description":"VAT of the package","name":"vat","in":"query"}],"responses":{"200":{"description":"Packages filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/package"}}},"400":{"description":"Invalid price or vat on query"},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get packages"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Creates a new package (must have coordinator credentials)","operationId":"createPackage","parameters":[{"description":"New package data","name":"package","in":"body","required":true,"schema":{"type":"object","required":["name","items","price","vat"],"properties":{"items":{"type":"object","properties":{"item":{"type":"string"},"public":{"type":"boolean"},"quantity":{"type":"integer"}}},"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Created package","schema":{"$ref":"#/definitions/package"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Unable to create the package"}}}},"/packages/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Get package by ID","operationId":"getPackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Package with the specific ID","schema":{"$ref":"#/definitions/package"}},"401":{"description":"Unauthorized"},"404":{"description":"Package not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Updates a specific package (must have coordinator credentials)","operationId":"updatePackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true},{"description":"New package data","name":"package","in":"body","required":true,"schema":{"type":"object","required":["name","price","vat"],"properties":{"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Updated package","schema":{"$ref":"#/definitions/package"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Invalid package ID"},"417":{"description":"Unable to update the package"}}}},"/packages/{id}/items":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages","items"],"summary":"Updates a specific package's items (must have coordinator credentials)","operationId":"updatePackageItems","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true},{"description":"New package data","name":"package","in":"body","required":true,"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"quantity":{"type":"integer"}}}}}}}],"responses":{"200":{"description":"Updated package","schema":{"$ref":"#/definitions/package"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Invalid package ID"},"417":{"description":"Unable to update the package"}}}},"/posts/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["posts"],"summary":"Get post by ID","operationId":"getPost","parameters":[{"type":"string","description":"ID of the post","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Post with the specific ID","schema":{"$ref":"#/definitions/post"}},"401":{"description":"Unauthorized"},"404":{"description":"Post not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["posts"],"summary":"Update post by ID","operationId":"updatePost","parameters":[{"type":"string","description":"ID of the post","name":"id","in":"path","required":true},{"description":"Data need for post update","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}}],"responses":{"200":{"description":"Post with the updated data","schema":{"$ref":"#/definitions/post"}},"400":{"description":"Bad payload"},"401":{"description":"Unauthorized"},"404":{"description":"Post not found"},"417":{"description":"Unable to update post"}}}},"/public/companies":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["companies","public"],"summary":"Public endpoint for getting all companies","operationId":"getCompaniesPublic","parameters":[{"type":"string","description":"Name of the company","name":"name","in":"query"},{"type":"integer","description":"ID of the event","name":"event","in":"query"},{"type":"boolean","description":"Companies participating as partner","name":"partner","in":"query"}],"responses":{"200":{"description":"Companies filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicCompany"}}},"417":{"description":"Unable to get companies"}}}},"/public/companies/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","public"],"summary":"Get public company by ID","operationId":"getCompanyPublic","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Public company with the specific ID","schema":{"$ref":"#/definitions/publicCompany"}},"401":{"description":"Unauthorized"},"404":{"description":"company not found"}}}},"/public/events":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["events","public"],"summary":"Public endpoint for getting all events","operationId":"getEventsPublic","parameters":[{"type":"boolean","description":"Get current event\nOn empty query (both current and pastEvents), returns all the events","name":"current","in":"query"},{"type":"boolean","description":"Get all the events except the current one\nOn empty query (both current and pastEvents), returns all the events","name":"pastEvents","in":"query"}],"responses":{"200":{"description":"Events filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicEvent"}}},"400":{"description":"Unable to make query"},"417":{"description":"Unable to get events"}}}},"/public/events/latest":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["events","public"],"summary":"Public endpoint for getting latest event","operationId":"getLatestEvent","responses":{"200":{"description":"Latest event","schema":{"$ref":"#/definitions/event"}},"417":{"description":"Unable to get event"}}}},"/public/members":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["members","public"],"summary":"Get all members, based on query","operationId":"getMembersPublic","parameters":[{"type":"string","description":"Name of the member","name":"name","in":"query"},{"type":"integer","format":"int64","description":"Member from this event","name":"event","in":"query"}],"responses":{"200":{"description":"Members filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicMember"}}},"417":{"description":"Unable to get members"}}}},"/public/sessions":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["sessions","public"],"summary":"Get all sessions, based on query","operationId":"getSessionsPublic","parameters":[{"type":"integer","format":"int64","description":"Session from this event","name":"event","in":"query"},{"enum":["TALK","PRESENTATION","WORKSHOP"],"type":"string","description":"Kind of session","name":"kind","in":"query"}],"responses":{"200":{"description":"Sessions filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicSession"}}},"417":{"description":"Unable to get sessions"}}}},"/public/sessions/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions","public"],"summary":"Get public session by ID","operationId":"getSessionPublic","parameters":[{"type":"string","description":"ID of the session","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Public session with the specific ID","schema":{"$ref":"#/definitions/publicSession"}},"401":{"description":"Unauthorized"},"404":{"description":"session not found"}}}},"/public/speakers":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","public"],"summary":"Public endpoint for getting all speakers","operationId":"getSpeakersPublic","parameters":[{"type":"string","description":"Name of the speaker","name":"name","in":"query"},{"type":"integer","description":"ID of the event","name":"event","in":"query"}],"responses":{"200":{"description":"Speakers filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicSpeaker"}}},"417":{"description":"Unable to get speakers"}}}},"/public/speakers/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","public"],"summary":"Get public speaker by ID","operationId":"getSpeakerPublic","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Public speaker with the specific ID","schema":{"$ref":"#/definitions/publicSpeaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}}},"/sessions":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Get sessions","operationId":"getSessions","parameters":[{"type":"integer","format":"int64","description":"Session from this event","name":"event","in":"query"},{"type":"string","format":"date-time","description":"Session began before this date","name":"before","in":"query"},{"type":"string","format":"date-time","description":"Session ended after this date","name":"after","in":"query"},{"type":"string","description":"Session happened on this location inside the venue","name":"place","in":"query"},{"enum":["TALK","PRESENTATION","WORKSHOP"],"type":"string","description":"Kind of session","name":"kind","in":"query"},{"type":"string","description":"Session given by this company","name":"company","in":"query"},{"type":"string","description":"Session given by this speaker","name":"speaker","in":"query"}],"responses":{"200":{"description":"Sessions filtered by query","schema":{"type":"array","items":{"$ref":"#/definitions/session"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to make query"}}}},"/sessions/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Get session by ID","operationId":"getSession","parameters":[{"type":"string","description":"ID of the session","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Session with the specific ID","schema":{"$ref":"#/definitions/session"}},"401":{"description":"Unauthorized"},"404":{"description":"Session not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Updates session (must have coordinator credentials)","operationId":"updateSession","parameters":[{"type":"string","description":"ID of the session","name":"id","in":"path","required":true},{"description":"New session data\n - kind: TALK, PRESENTATION or WORKSHOP\n - if kind=TALK, then speaker must be specified\n - if kind=PRESENTATION or kind=WORKSHOP, then company must be specified\n - place is optional\n - videoURL is optional","name":"session","in":"body","required":true,"schema":{"type":"object","required":["begin","end","title","description","kind"],"properties":{"begin":{"type":"string","format":"date-time"},"company":{"type":"string"},"description":{"type":"string"},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"type":"string"},"tickets":{"type":"object","properties":{"end":{"type":"string","format":"date-time"},"max":{"type":"integer"},"start":{"type":"string","format":"date-time"}}},"title":{"type":"string"},"videoURL":{"type":"string"}}}}],"responses":{"200":{"description":"Updated session","schema":{"$ref":"#/definitions/session"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Session not found"},"417":{"description":"Unable to update session"}}}},"/speakers":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Get all speakers","operationId":"getSpeakers","parameters":[{"type":"string","description":"Name of the speaker","name":"name","in":"query"},{"type":"integer","description":"Has a participation entry for this event","name":"event","in":"query"},{"type":"string","description":"Was contacted by this member","name":"member","in":"query"}],"responses":{"200":{"description":"Speakers filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/speaker"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get speakers"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Create a new speaker","operationId":"createSpeaker","parameters":[{"description":"Information needed to create the new speaker.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","title","bio"],"properties":{"bio":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Created speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid input or couldn't create the new speaker"},"401":{"description":"Unauthorized"}}}},"/speakers/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Get speaker by ID","operationId":"getSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Speaker with the specific ID","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker by ID","operationId":"updateSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"Information needed to update the speaker.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","bio","title","notes"],"properties":{"bio":{"type":"string"},"name":{"type":"string"},"notes":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker"}}}},"/speakers/{id}/image/internal":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's internal image by ID","operationId":"updateSpeakerInternalImage","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"file","description":"Photo of speaker","name":"image","in":"formData"}],"responses":{"200":{"description":"Speaker with the updated data","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to perform operation"}}}},"/speakers/{id}/image/public/company":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's company public image by ID (must have at least coordinator credentials)","operationId":"updateSpeakerCompanyImage","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"file","description":"Logo of speaker's company","name":"image","in":"formData"}],"responses":{"200":{"description":"Speaker with the updated data","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to perform operation"}}}},"/speakers/{id}/image/public/speaker":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's public image by ID (must have at least coordinator credentials)","operationId":"updateSpeakerPublicImage","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"file","description":"Photo of speaker","name":"image","in":"formData"}],"responses":{"200":{"description":"Speaker with the updated data","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to perform operation"}}}},"/speakers/{id}/participation":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's participation on the current event","operationId":"updateSpeakerParticipation","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","feedback","room"],"properties":{"feedback":{"type":"string"},"member":{"type":"string"},"room":{"type":"object","properties":{"cost":{"type":"integer"},"notes":{"type":"string"},"type":{"type":"string"}}}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker's participation data"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Adds participation on the current event to a speaker","operationId":"addSpeakerParticipation","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Unable to add participation for the current event to this speaker"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Removes a speakers' participation from the current event. Admin only and must have no communications and not be associated with any session ","operationId":"removeSpeakerParticipation","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to remove speaker's participation: Has communications or is associated with a session"}}}},"/speakers/{id}/participation/flightInfo":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","flightInfo"],"summary":"Adds flightInfo to a speaker's participation","operationId":"addSpeakerFlightInfo","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"Information needed to create a flight info.\n - Inbound/outbound: airports\n - Link: URL to the flight company's flight\n - Cost: in cents (€)","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["inbound","outbound","from","to","bought","cost","notes"],"properties":{"bought":{"type":"boolean"},"cost":{"description":"In cents (€)","type":"integer"},"from":{"description":"Airport","type":"string"},"inbound":{"type":"string","format":"date-time"},"link":{"description":"URL to the flight","type":"string"},"notes":{"type":"string"},"outbound":{"type":"string","format":"date-time"},"to":{"description":"Airport","type":"string"}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to create flight info, or add it to the speaker's participation"}}}},"/speakers/{id}/participation/flightInfo/{flightInfoID}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","flightInfo"],"summary":"Removes flightInfo from a speaker's participation, and deletes it from the database (must have at least coordinator credentials)","operationId":"removeSpeakerFlightInfo","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"string","description":"ID of the flightInfo","name":"flightInfoID","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to remove or delete flight info"}}}},"/speakers/{id}/participation/status/next":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Gets all the valid steps to be taken on a speaker's participation status on the current event","operationId":"getvalidSpeakerParticipationStatusSteps","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Valid steps","schema":{"type":"object","properties":{"steps":{"type":"array","items":{"type":"object","properties":{"next":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"step":{"type":"integer"}}}}}}},"400":{"description":"Speaker without participation on the current event"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}}},"/speakers/{id}/participation/status/{status}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Update a speaker's participation status on the current event (admin credentials)","operationId":"updateSpeakerParticipationStatus","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"],"type":"string","description":"New status","name":"status","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker's participation status"}}}},"/speakers/{id}/participation/status/{step}":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Step a speaker's participation status on the current event","operationId":"stepSpeakerParticipationStatus","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"integer","description":"Step to the next status","name":"step","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker's participation status"}}}},"/speakers/{id}/subscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Subscribe to speaker by ID","operationId":"subscribeToSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to subscribe to speaker"}}}},"/speakers/{id}/thread":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","threads"],"summary":"Adds thread on the current event to a speaker","operationId":"addSpeakerThread","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"New thread information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text","kind"],"properties":{"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"meeting":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"participants":{"type":"object","properties":{"companyReps":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"string"}}}},"place":{"type":"string"}}},"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload, or invalid credentials"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to create post, create thread or add created thread to speaker participation"}}}},"/speakers/{id}/unsubscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Unsubscribe to speaker by ID","operationId":"unsubscribeToSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to unsubscribe from speaker"}}}},"/teams":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Get all teams","operationId":"getTeams","parameters":[{"type":"string","description":"Name of the team","name":"name","in":"query"},{"type":"string","description":"Contains this member","name":"member","in":"query"},{"type":"string","description":"Contains all the members whose name match the given on this query","name":"memberName","in":"query"},{"type":"integer","format":"int64","description":"Team from this event","name":"event","in":"query"}],"responses":{"200":{"description":"Teams filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/team"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get teams"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Create a new team (must have coordinator credentials). Created teams are added to the current event","operationId":"createTeam","parameters":[{"description":"Information needed to create the new team.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}],"responses":{"200":{"description":"Created team.","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Invalid input or couldn't create the new team."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/teams/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Get team by ID","operationId":"getTeam","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Team with the specific ID","schema":{"$ref":"#/definitions/team"}},"401":{"description":"Unauthorized"},"404":{"description":"Team not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Updates a teams's name (must have coordinator credentials)","operationId":"updateTeam","parameters":[{"description":"New team data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}},{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated team","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Invalid payload, or couldn't update team"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Couldn't find team"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Deletes a team (must have admin credentials)","operationId":"deleteTeam","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted team","schema":{"$ref":"#/definitions/team"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team not found"}}}},"/teams/{id}/meetings":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","meetings"],"summary":"Creates a meeting and adds it to a team","operationId":"addTeamMeeting","parameters":[{"description":"New meeting data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["begin","end","local"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"local":{"type":"string"}}}},{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated team","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Invalid payload, or couldn't create meeting"},"401":{"description":"Unauthorized"},"417":{"description":"Couldn't find team"}}}},"/teams/{id}/meetings/{meetingID}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","meetings"],"summary":"Removes a meeting from a team (must have at least Team Leader credentials)","operationId":"deleteTeamMeeting","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"type":"string","description":"ID of the meeting","name":"meetingID","in":"path","required":true}],"responses":{"200":{"description":"Removed meeting","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Couldn't find team or meeting, or meeting not on team"},"417":{"description":"Couldn't find team"}}}},"/teams/{id}/members":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","members"],"summary":"Add a new member to the team (must have coordinator credentials)","operationId":"addTeamMember","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"description":"New member data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","role"],"properties":{"member":{"type":"string"},"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]}}}}],"responses":{"200":{"description":"Team with the added member","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Bad name or role on payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team or member not found"}}}},"/teams/{id}/members/{memberID}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","members"],"summary":"Updates a member's role on the team (must have coordinator credentials)","operationId":"updateTeamMemberRole","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"type":"string","description":"ID of the member","name":"memberID","in":"path","required":true},{"description":"New member data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","role"],"properties":{"role":{"type":"string"}}}}],"responses":{"200":{"description":"Team with the updated member","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Bad name or role on payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team or member not found"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","members"],"summary":"Removes a member from the team (must have coordinator credentials)","operationId":"deleteTeamMember","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"type":"string","description":"ID of the member","name":"memberID","in":"path","required":true}],"responses":{"200":{"description":"Team without the removed member","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Bad name or role on payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team or member not found"}}}},"/threads/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads"],"summary":"Get thread by ID","operationId":"getThread","parameters":[{"type":"string","description":"ID of the thread","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Thread with the specific ID","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"404":{"description":"Thread not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads"],"summary":"Updates a thread. Only valid if you own the thread (or admin)","operationId":"updateThread","parameters":[{"type":"string","description":"ID of the thread","name":"id","in":"path","required":true},{"description":"Update data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["kind","meeting"],"properties":{"kind":{"type":"string"},"meeting":{"type":"string"}}}}],"responses":{"200":{"description":"Updated thread","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"403":{"description":"Authtenticated, but access level is not enough"},"404":{"description":"Thread not found"}}}},"/threads/{id}/comments":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads","posts"],"summary":"Add comment to thread","operationId":"addCommentToThread","parameters":[{"type":"string","description":"ID of the thread","name":"id","in":"path","required":true},{"description":"Comment data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated Thread","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"417":{"description":"Thread not found, or unable to create and/or add post to thread"}}}},"/threads/{threadID}/comments/{postID}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads","posts"],"summary":"Remove comment from thread","operationId":"removeCommentFromThread","parameters":[{"type":"string","description":"ID of the thread","name":"threadID","in":"path","required":true},{"type":"string","description":"ID of the post","name":"postID","in":"path","required":true}],"responses":{"200":{"description":"Updated Thread","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"404":{"description":"Thread or post not found"},"417":{"description":"Unable to remove post from thread"}}}}},"definitions":{"billing":{"type":"object","properties":{"company":{"type":"string"},"emission":{"type":"string","format":"date-time"},"event":{"type":"integer"},"id":{"type":"string"},"invoiceNumber":{"type":"integer"},"notes":{"type":"string"},"status":{"type":"object","properties":{"invoice":{"type":"boolean"},"paid":{"type":"boolean"},"proForma":{"type":"boolean"},"receipt":{"type":"boolean"}}},"value":{"type":"integer"},"visible":{"type":"boolean"}}},"company":{"type":"object","properties":{"billingInfo":{"type":"object","properties":{"address":{"type":"string"},"name":{"type":"string"},"tin":{"type":"string"}}},"description":{"type":"string"},"employers":{"type":"array","items":{"type":"string"}},"id":{"type":"string"},"imgs":{"type":"object","properties":{"internal":{"type":"string"},"public":{"type":"string"}}},"name":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"billing":{"type":"string"},"communications":{"type":"array","items":{"type":"string"}},"confirmed":{"type":"string","format":"date-time"},"event":{"type":"integer","format":"int64"},"member":{"type":"string"},"notes":{"type":"string"},"package":{"type":"string"},"partner":{"type":"boolean"},"status":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"subscribers":{"type":"array","items":{"type":"string"}}}}},"site":{"type":"string"}}},"companyRep":{"type":"object","properties":{"contact":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}},"contact":{"type":"object","properties":{"id":{"type":"string"},"mails":{"type":"array","items":{"type":"object","properties":{"mail":{"type":"string"},"personal":{"type":"boolean"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"event":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"id":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"type":"string"}},"meetings":{"type":"array","items":{"type":"string"}},"name":{"type":"string"},"packages":{"type":"array","items":{"type":"object","properties":{"available":{"type":"boolean"},"public_name":{"type":"string"},"template":{"type":"string"}}}},"sessions":{"type":"array","items":{"type":"string"}},"teams":{"type":"array","items":{"type":"string"}},"themes":{"type":"array","items":{"type":"string"}}}},"flightInfo":{"type":"object","properties":{"bought":{"type":"boolean"},"cost":{"type":"integer"},"from":{"type":"string"},"id":{"type":"string"},"inbound":{"type":"string","format":"date-time"},"link":{"type":"string"},"notes":{"type":"string"},"outbound":{"type":"string","format":"date-time"},"to":{"type":"string"}}},"item":{"type":"object","properties":{"description":{"type":"string"},"id":{"type":"string"},"img":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer"},"type":{"type":"string"},"vat":{"type":"integer"}}},"meeting":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"id":{"type":"string"},"minute":{"type":"string"},"participants":{"type":"object","properties":{"companyReps":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"string"}}}},"place":{"type":"string"}}},"member":{"type":"object","properties":{"contact":{"type":"string"},"id":{"type":"string"},"img":{"type":"string"},"istid":{"type":"string"},"name":{"type":"string"},"sinfoid":{"type":"string"}}},"memberParticipation":{"type":"object","properties":{"event":{"type":"integer"},"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]},"team":{"type":"string"}}},"notification":{"type":"object","properties":{"company":{"type":"string"},"date":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["CREATED","UPDATED","TAGGED","DELETED"]},"meeting":{"type":"string"},"member":{"type":"string"},"post":{"type":"string"},"session":{"type":"string"},"signature":{"type":"string"},"speaker":{"type":"string"}}},"package":{"type":"object","properties":{"id":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"public":{"type":"boolean"},"quantity":{"type":"integer"}}}},"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}},"post":{"type":"object","properties":{"id":{"type":"string"},"member":{"type":"string"},"posted":{"type":"string","format":"date-time"},"text":{"type":"string"},"updated":{"type":"string","format":"date-time"}}},"publicCompany":{"type":"object","properties":{"id":{"type":"string"},"img":{"type":"string"},"name":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"event":{"type":"integer"},"package":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"quantity":{"type":"integer"}}}},"name":{"type":"string"}}},"partner":{"type":"boolean"}}}},"site":{"type":"string"}}},"publicEvent":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"themes":{"type":"array","items":{"type":"string"}}}},"publicMember":{"type":"object","properties":{"img":{"type":"string"},"name":{"type":"string"},"socials":{"type":"object","properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"publicSession":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"company":{"$ref":"#/definitions/publicCompany"},"description":{"type":"string"},"end":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"$ref":"#/definitions/publicSpeaker"},"tickets":{"type":"object","properties":{"end":{"type":"string","format":"date-time"},"max":{"type":"integer"},"start":{"type":"string","format":"date-time"}}},"title":{"type":"string"},"videoURL":{"type":"string"}}},"publicSpeaker":{"type":"object","properties":{"id":{"type":"string"},"imgs":{"type":"object","properties":{"company":{"type":"string"},"speaker":{"type":"string"}}},"name":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"event":{"type":"integer"},"feedback":{"type":"string"}}}},"title":{"type":"string"}}},"session":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"company":{"type":"string"},"description":{"type":"string"},"end":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"type":"string"},"tickets":{"type":"object","properties":{"end":{"type":"string","format":"date-time"},"max":{"type":"integer"},"start":{"type":"string","format":"date-time"}}},"title":{"type":"string"},"videoURL":{"type":"string"}}},"speaker":{"type":"object","properties":{"bio":{"type":"string"},"contact":{"type":"string"},"id":{"type":"string"},"imgs":{"type":"object","properties":{"company":{"type":"string"},"internal":{"type":"string"},"speaker":{"type":"string"}}},"name":{"type":"string"},"notes":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"communications":{"type":"array","items":{"type":"string"}},"event":{"type":"integer"},"feedback":{"type":"string"},"flights":{"type":"array","items":{"type":"string"}},"member":{"type":"string"},"room":{"type":"object","properties":{"cost":{"type":"integer"},"notes":{"type":"string"},"type":{"type":"string"}}},"status":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"subscribers":{"type":"array","items":{"type":"string"}}}}},"title":{"type":"string"}}},"team":{"type":"object","properties":{"id":{"type":"string"},"meetings":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"object","properties":{"member":{"type":"string"},"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]}}}},"name":{"type":"string"}}},"thread":{"type":"object","properties":{"comments":{"type":"array","items":{"type":"string"}},"entry":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"meeting":{"type":"string"},"posted":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["APPROVED","REVIEWED","PENDING"]}}}},"securityDefinitions":{"Bearer":{"type":"apiKey","name":"Authorization","in":"header"}},"tags":[{"description":"Public endpoints (don't need authentication)","name":"public"},{"description":"Authentication endpoints","name":"auth"},{"description":"Saved bills","name":"billings"},{"description":"Contacted companies","name":"companies"},{"description":"Person representing a certain company","name":"companyReps"},{"description":"All of our saved contacts","name":"contacts"},{"description":"Information relative to each event","name":"events"},{"description":"Saved flight information","name":"flightInfo"},{"description":"Items in packages sold by SINFO","name":"items"},{"description":"Saved meetings' information","name":"meetings"},{"description":"Member's personal endpoints","name":"me"},{"description":"Information related to each SINFO's member","name":"members"},{"description":"Notifications for our members to keep up with updated information","name":"notifications"},{"description":"Packages sold/traded by SINFO to companies","name":"packages"},{"description":"Messages shared by SINFO's members","name":"posts"},{"description":"Scheduled sessions for the event's week, such as keynotes, presentations, etc","name":"sessions"},{"description":"Speakers for the event's keynotes","name":"speakers"},{"description":"SINFO's teams","name":"teams"},{"description":"Additional communication taken inside the posts","name":"threads"}],"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"}} \ No newline at end of file +{"schemes":["http"],"swagger":"2.0","info":{"description":"SINFO's internal management application\n","title":"Swagger Deck2","termsOfService":"http://swagger.io/terms/","contact":{"email":"devteam@sinfo.org"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.0"},"host":"localhost:8080","basePath":"/","paths":{"/auth/callback":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["auth"],"summary":"Authentication callback endpoint","operationId":"authCallback","responses":{"317":{"description":"Redirects to deck2 website with authentication code"},"401":{"description":"Unauthorized (reason in the body of the response)"}}}},"/auth/login":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["auth"],"summary":"Authenticates member","operationId":"authLogin","parameters":[{"type":"string","description":"URl to be redirected after the successful login\n\n\u003cb\u003eExample:\u003c/b\u003e \n \u003ccode\u003eGET /auth/login?redirect=example.com/some/path\u003c/code\u003e \n On a successful login, you'll be redirected to \u003ccode\u003eexample.com/some/path/{generated_authentication_token}\u003c/code\u003e","name":"redirect","in":"query"}],"responses":{"317":{"description":"Redirects to authentication server"}}}},"/auth/verify/{token}":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["auth"],"summary":"Authentication verification endpoint","operationId":"verifyToken","parameters":[{"type":"string","description":"JWT token sent to be verified","name":"token","in":"path","required":true}],"responses":{"200":{"description":"Token is valid"},"401":{"description":"Token in invalid, therefore unauthorized"}}}},"/billings":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Get all billings. If role is lower than coordinator, only visible billings will be returned","operationId":"getBillings","parameters":[{"type":"string","format":"date-time","description":"Billings emited after date","name":"after","in":"query"},{"type":"string","format":"date-time","description":"Billings emited before date","name":"before","in":"query"},{"type":"string","description":"Billings with value greater than this value","name":"valueGreaterThan","in":"query"},{"type":"string","description":"Billings with value lower than this value","name":"valueLessThan","in":"query"},{"type":"string","description":"Billings from this event","name":"event","in":"query"},{"type":"string","description":"Billings from this company","name":"company","in":"query"}],"responses":{"200":{"description":"Billings filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/billing"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get billings"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Creates a new billing","operationId":"createBilling","parameters":[{"description":"Information needed to create the new company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["status","event","value","invoiceNumber","emission","notes"],"properties":{"emission":{"type":"string","format":"date-time"},"event":{"type":"integer"},"invoiceNumber":{"type":"string"},"notes":{"type":"string"},"status":{"type":"object","required":["proForma","paid","receipt","invoice"],"properties":{"invoice":{"type":"boolean"},"paid":{"type":"boolean"},"proForma":{"type":"boolean"},"receipt":{"type":"boolean"}}},"value":{"type":"integer"}}}}],"responses":{"200":{"description":"Created billing","schema":{"$ref":"#/definitions/billing"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Couldn't find created billing"}}}},"/billings/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Gets a billing","operationId":"getBilling","parameters":[{"type":"string","description":"ID of billing","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Specified Billing","schema":{"$ref":"#/definitions/billing"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Billing not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["billings"],"summary":"Updates a billing","operationId":"updateBilling","parameters":[{"type":"string","description":"ID of billing","name":"id","in":"path","required":true},{"description":"Information needed to create the new company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["status","event","value","invoiceNumber","emission","notes"],"properties":{"emission":{"type":"string","format":"date-time"},"event":{"type":"integer"},"invoiceNumber":{"type":"string"},"notes":{"type":"string"},"status":{"type":"object","required":["proForma","paid","receipt","invoice"],"properties":{"invoice":{"type":"boolean"},"paid":{"type":"boolean"},"proForma":{"type":"boolean"},"receipt":{"type":"boolean"}}},"value":{"type":"integer"}}}}],"responses":{"200":{"description":"Updated billing","schema":{"$ref":"#/definitions/billing"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Couldn't find specified billing"}}}},"/companies":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Get all companies","operationId":"getCompanies","parameters":[{"type":"string","description":"Name of the company","name":"name","in":"query"},{"type":"integer","description":"Has a participation entry for this event","name":"event","in":"query"},{"type":"string","description":"Was contacted by this member","name":"member","in":"query"},{"type":"boolean","description":"Participated as a partner","name":"partner","in":"query"}],"responses":{"200":{"description":"Companies filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/company"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get companies"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Create a new company","operationId":"createCompany","parameters":[{"description":"Information needed to create the new company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","description","site"],"properties":{"description":{"type":"string"},"name":{"type":"string"},"site":{"type":"string"}}}}],"responses":{"200":{"description":"Created company.","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid input or couldn't create the new company"},"401":{"description":"Unauthorized"}}}},"/companies/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Get company by ID","operationId":"getCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Company with the specific ID","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Update company by ID","operationId":"updateCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"Information needed to update the company.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","description","site"],"properties":{"billingInfo":{"type":"object","properties":{"address":{"type":"string"},"name":{"type":"string"},"tin":{"type":"string"}}},"description":{"type":"string"},"name":{"type":"string"},"site":{"type":"string"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to update company"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Delete company by ID (must have admin credentials)","operationId":"deleteCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Unable to delete company"}}}},"/companies/{id}/employer":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","companyReps","contacts"],"summary":"Creates a new companyRep and adds it to a company","operationId":"addEmployer","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"Information needed to create the new companyRep.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"contact":{"type":"object","required":["phones","socials","mails"],"properties":{"mails":{"type":"array","items":{"type":"object","required":["mail","valid","personal"],"properties":{"personal":{"type":"boolean"},"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","required":["phone","valid"],"properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","required":["facebook","skype","github","twitter","linkedin"],"properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Couldn't find company or created company rep or created contact"}}}},"/companies/{id}/employer/{rep}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","companyReps"],"summary":"Deletes a companyRep and removes it from company ","operationId":"removeEmployer","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"string","description":"ID of the companyRep","name":"rep","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Couldn't find company or company rep"}}}},"/companies/{id}/image/internal":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["companies"],"summary":"Update company's internal image by ID","operationId":"updateCompanyInternalImage","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"file","description":"Logo of company","name":"image","in":"formData"}],"responses":{"200":{"description":"Company with the updated data","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to perform operation"}}}},"/companies/{id}/image/public":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["companies"],"summary":"Update company's public image by ID (must have coordination credentials)","operationId":"updateCompanyPublicImage","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"file","description":"Logo of company","name":"image","in":"formData"}],"responses":{"200":{"description":"Company with the updated data","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Company not found"},"417":{"description":"Unable to perform operation"}}}},"/companies/{id}/participation":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Updates participation data on the current event to a company","operationId":"updateCompanyParticipation","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","partner","confirmed","notes"],"properties":{"confirmed":{"type":"string","format":"date-time"},"member":{"type":"string"},"notes":{"type":"string"},"partner":{"type":"boolean"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Unable to add participation for the current event to this company"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Adds participation on the current event to a company","operationId":"addCompanyParticipation","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["partner"],"properties":{"partner":{"type":"boolean"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}}},"/companies/{id}/participation/package":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","packages"],"summary":"Adds a package on the current event to a company (must have at least coordination credentails)","operationId":"addCompanyPackage","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","items","price","vat"],"properties":{"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"public":{"type":"boolean"},"quantity":{"type":"integer"}}}},"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Company not found"},"417":{"description":"Couldn't add package to company"}}}},"/companies/{id}/participation/status/next":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Gets all the valid steps to be taken on a company's participation status on the current event","operationId":"getvalidCompanyParticipationStatusSteps","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Valid steps","schema":{"type":"object","properties":{"steps":{"type":"array","items":{"type":"object","properties":{"next":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"step":{"type":"integer"}}}}}}},"400":{"description":"Company without participation on the current event"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"}}}},"/companies/{id}/participation/status/{status}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Updated a company's participation status on the current event (admin credentials)","operationId":"updateCompanyParticipationStatus","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"],"type":"string","description":"New status","name":"status","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Company not found"},"417":{"description":"Unable to update company's participation status"}}}},"/companies/{id}/participation/status/{step}":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Step a company's participation status on the current event","operationId":"stepCompanyParticipationStatus","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"type":"integer","description":"Step to the next status","name":"step","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to update company's participation status"}}}},"/companies/{id}/subscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Subscribe to company by ID","operationId":"subscribeToCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to subscribe to company"}}}},"/companies/{id}/thread":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","threads"],"summary":"Adds thread on the current event to a company","operationId":"addCompanyThread","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true},{"description":"New thread information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text","kind"],"properties":{"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"meeting":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"participants":{"type":"object","properties":{"companyReps":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"string"}}}},"place":{"type":"string"}}},"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"400":{"description":"Invalid payload, or invalid credentials"},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to create post, create thread or add created thread to company participation"}}}},"/companies/{id}/unsubscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies"],"summary":"Unsubscribe to company by ID","operationId":"unsubscribeToCompany","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated company","schema":{"$ref":"#/definitions/company"}},"401":{"description":"Unauthorized"},"404":{"description":"Company not found"},"417":{"description":"Unable to unsubscribe from company"}}}},"/companyReps":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companyReps"],"summary":"Get all companyReps, based on query","operationId":"getCompanyReps","parameters":[{"type":"string","description":"Name of the companyRep","name":"name","in":"query"}],"responses":{"200":{"description":"CompanyReps filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/companyRep"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get companyReps"}}}},"/companyReps/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companyReps"],"summary":"Gets a companyRep","operationId":"getCompanyRep","parameters":[{"type":"string","description":"ID of the companyRep","name":"id","in":"path","required":true}],"responses":{"200":{"description":"CompanyRep with specified ID","schema":{"$ref":"#/definitions/companyRep"}},"401":{"description":"Unauthorized"},"404":{"description":"CompanyRep not found"},"417":{"description":"Unable to get companyRep"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companyReps"],"summary":"Updates a companyRep","operationId":"updateCompanyRep","parameters":[{"type":"string","description":"ID of the companyrep","name":"id","in":"path","required":true},{"description":"Information needed to create the new companyRep.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"contact":{"type":"object","required":["phones","socials","mails"],"properties":{"mails":{"type":"array","items":{"type":"object","required":["mail","valid","personal"],"properties":{"personal":{"type":"boolean"},"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","required":["phone","valid"],"properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","required":["facebook","skype","github","twitter","linkedin"],"properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated companyRep ","schema":{"$ref":"#/definitions/companyRep"}},"401":{"description":"Unauthorized"},"404":{"description":"CompanyRep not found"},"417":{"description":"Unable to get companies"}}}},"/contacts":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["contacts"],"summary":"Get all contacts","operationId":"getContacts","parameters":[{"type":"string","description":"Partial and case insensitive match for phone","name":"phone","in":"query"},{"type":"string","description":"Partial and case insensitive match for mail","name":"mail","in":"query"}],"responses":{"200":{"description":"Contacts filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/contact"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get contacts"}}}},"/contacts/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["contacts"],"summary":"Get contact by ID","operationId":"getContact","parameters":[{"type":"string","description":"ID of the contact","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Contact with the specific ID","schema":{"$ref":"#/definitions/contact"}},"401":{"description":"Unauthorized"},"404":{"description":"contact not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["contacts"],"summary":"Updates a contact","operationId":"updateContact","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true},{"description":"New contact data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["phones","socials","mails"],"properties":{"mails":{"type":"array","items":{"type":"object","required":["mail","valid","personal"],"properties":{"personal":{"type":"boolean"},"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","required":["phone","valid"],"properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","required":["facebook","skype","github","twitter","linkedin"],"properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}}}],"responses":{"200":{"description":"Updated contact.","schema":{"$ref":"#/definitions/contact"}},"400":{"description":"Invalid input or couldn't updated the contact."},"401":{"description":"Unauthorized"}}}},"/events":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Get all events","operationId":"getEvents","parameters":[{"type":"string","description":"Name of the event","name":"name","in":"query"},{"type":"string","format":"date-time","description":"Event happened before this date","name":"before","in":"query"},{"type":"string","format":"date-time","description":"Event happened after this date","name":"after","in":"query"},{"type":"string","format":"date-time","description":"Event happened during this date","name":"during","in":"query"}],"responses":{"200":{"description":"Events filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/event"}}},"400":{"description":"Invalid date format on query"},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get events"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Update the current event (must have coordinator credentials)","operationId":"updateEvent","parameters":[{"description":"New event data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload, or couldn't update event"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Unable to update event"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Create a new event (must have admin credentials)","operationId":"createEvent","parameters":[{"description":"Information needed to create the new event.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}],"responses":{"200":{"description":"Created event. The new ID will be an increment to the current event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid input or couldn't create the new event"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/events/items":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","items"],"summary":"Adds an item to the current event (must have coordinator credentials)","operationId":"addEventItem","parameters":[{"description":"Item to store on the current event","name":"item","in":"body","required":true,"schema":{"type":"object","required":["id"],"properties":{"item":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found, or item not found"},"417":{"description":"Unable to add item"}}}},"/events/items/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","items"],"summary":"Removes item from the current event's packages (must have coordinator credentials)","operationId":"removeEventItem","parameters":[{"type":"string","description":"ID of the item","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Event with the removed item","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to remove item from event"}}}},"/events/meetings":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","meetings"],"summary":"Creates and adds a new meeting to the current event (must have coordinator credentials)","operationId":"addEventMeeting","parameters":[{"description":"Meeting to store on the current event","name":"meeting","in":"body","required":true,"schema":{"type":"object","required":["title","kind","begin","end","place"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["EVENT","TEAM","COMPANY"]},"place":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to create or add meeting"}}}},"/events/meetings/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","meetings"],"summary":"Removes and deletes a meeting from the current event (must have coordinator credentials)","operationId":"removeMeetingFromEvent","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found or meeting not found"},"417":{"description":"Unable to remove or delete meeting"}}}},"/events/packages":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","packages"],"summary":"Add template to packages of the current event and make it available (must have coordinator credentials)","operationId":"addEventPackage","parameters":[{"description":"Package (template) to store on the current event","name":"template","in":"body","required":true,"schema":{"type":"object","required":["template","public_name"],"properties":{"public_name":{"type":"string"},"template":{"type":"string"}}}}],"responses":{"200":{"description":"Event with the updated packages","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found, or package not found"},"417":{"description":"Unable to save package on event"}}}},"/events/packages/{id}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","packages"],"summary":"Modifies template to packages on the current event (must have coordinator credentials)","operationId":"updateEventPackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true},{"description":"Updated template data","name":"template","in":"body","schema":{"type":"object","required":["template","public_name"],"properties":{"available":{"type":"boolean"},"public_name":{"type":"string"}}}}],"responses":{"200":{"description":"Event with the updated packages","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found or template not found"},"417":{"description":"Unable to update template on event"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","packages"],"summary":"Removes template to packages from the current event (must have coordinator credentials)","operationId":"removeEventPackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Event with the updated packages","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to remove package from event"}}}},"/events/sessions":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","sessions"],"summary":"Creates and adds a new session to the current event (must have coordinator credentials)","operationId":"addEventSession","parameters":[{"description":"Session to store on the current event\n - kind: TALK, PRESENTATION or WORKSHOP\n - if kind=TALK, then speaker must be specified\n - if kind=PRESENTATION or kind=WORKSHOP, then company must be specified\n - dinamizers is optional\n - space is optional","name":"session","in":"body","required":true,"schema":{"type":"object","required":["begin","end","title","description","kind"],"properties":{"begin":{"type":"string","format":"date-time"},"company":{"type":"string"},"description":{"type":"string"},"dinamizers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"position":{"type":"string"}}}},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to create or add session"}}}},"/events/teams/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events","teams"],"summary":"Removes (but does not delete) a team from the current event (must have admin credentials)","operationId":"removeTeamFromEvent","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated event","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found or team not found"},"417":{"description":"Unable to remove team"}}}},"/events/themes":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Update the current event's themes (must have coordinator credentials)","operationId":"updateEventThemes","parameters":[{"description":"Themes for the event. Must have the same number of elements as there are days in the duration of the event, or be empty","name":"themes","in":"body","required":true,"schema":{"type":"object","required":["themes"],"properties":{"themes":{"type":"array","items":{"type":"string"}}}}}],"responses":{"200":{"description":"Event with the updated themes","schema":{"$ref":"#/definitions/event"}},"400":{"description":"Invalid payload (be it format, or number of elements in the array)"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"},"417":{"description":"Unable to update event's themes"}}}},"/events/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Get event by ID","operationId":"getEvent","parameters":[{"type":"integer","description":"ID of the event","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Event with the specific ID","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"404":{"description":"Event not found"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["events"],"summary":"Deletes an event (must have admin credentials)","operationId":"deleteEvent","parameters":[{"type":"integer","description":"ID of the event","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted event","schema":{"$ref":"#/definitions/event"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Event not found"}}}},"/flightInfo/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["flightInfo"],"summary":"Get flight info by ID","operationId":"getFlightInfo","parameters":[{"type":"string","description":"ID of the flight info","name":"id","in":"path","required":true}],"responses":{"200":{"description":"FlightInfo with the specific ID","schema":{"$ref":"#/definitions/flightInfo"}},"401":{"description":"Unauthorized"},"404":{"description":"FlightInfo not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["flightInfo"],"summary":"Update flight info by ID","operationId":"updateFlightInfo","parameters":[{"type":"string","description":"ID of the flight info","name":"id","in":"path","required":true},{"description":"Information needed to create a flight info.\n - Inbound/outbound: airports\n - Link: URL to the flight company's flight\n - Cost: in cents (€)","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["inbound","outbound","from","to","bought","cost","notes"],"properties":{"bought":{"type":"boolean"},"cost":{"description":"In cents (€)","type":"integer"},"from":{"description":"Airport","type":"string"},"inbound":{"type":"string","format":"date-time"},"link":{"description":"URL to the flight","type":"string"},"notes":{"type":"string"},"outbound":{"type":"string","format":"date-time"},"to":{"description":"Airport","type":"string"}}}}],"responses":{"200":{"description":"FlightInfo with the specific ID","schema":{"$ref":"#/definitions/flightInfo"}},"400":{"description":"Bad payload"},"401":{"description":"Unauthorized"},"404":{"description":"FlightInfo not found"},"417":{"description":"Unable to update FlightInfo"}}}},"/items":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Get all items","operationId":"getItems","parameters":[{"type":"string","description":"Name of the item","name":"name","in":"query"},{"type":"string","description":"Type of the item","name":"type","in":"query"}],"responses":{"200":{"description":"Items filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/item"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get items"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Creates a new item (must have coordinator credentials)","operationId":"createItem","parameters":[{"description":"New item data","name":"item","in":"body","required":true,"schema":{"type":"object","required":["name","type","description","price","vat"],"properties":{"description":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer"},"type":{"type":"string"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Created item","schema":{"$ref":"#/definitions/item"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Unable to create the item"}}}},"/items/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Get item by ID","operationId":"getItem","parameters":[{"type":"string","description":"ID of the item","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Item with the specific ID","schema":{"$ref":"#/definitions/item"}},"401":{"description":"Unauthorized"},"404":{"description":"Item not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["items"],"summary":"Updates an item (must have coordinator credentials)","operationId":"updateItem","parameters":[{"description":"Information needed to update the item.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","type","description","price","vat"],"properties":{"description":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer"},"type":{"type":"string"},"vat":{"type":"integer"}}}},{"type":"string","description":"ID of the item","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated item.","schema":{"$ref":"#/definitions/item"}},"400":{"description":"Invalid input or couldn't updated the item."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Item not found"}}}},"/items/{id}/image":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["items"],"summary":"Update item image by ID","operationId":"uploadItemImage","parameters":[{"type":"string","description":"ID of the item","name":"id","in":"path","required":true},{"type":"file","description":"Photo of item","name":"image","in":"formData"}],"responses":{"200":{"description":"Item with the updated data","schema":{"$ref":"#/definitions/item"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Item not found"},"417":{"description":"Unable to perform operation"}}}},"/me":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["me"],"summary":"Get my information","operationId":"getMe","responses":{"200":{"description":"My information","schema":{"$ref":"#/definitions/member"}},"401":{"description":"Unauthorized"},"404":{"description":"Information not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["me"],"summary":"Updates my information","operationId":"updateMe","parameters":[{"description":"Information needed to update my information.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","istid"],"properties":{"istid":{"type":"string"},"name":{"type":"string"}}}}],"responses":{"200":{"description":"Updated information","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid input or couldn't updated my information."},"401":{"description":"Unauthorized"}}}},"/me/image":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["me"],"summary":"Update member's image","operationId":"updateMyImage","parameters":[{"type":"file","description":"Photo of the member","name":"image","in":"formData"}],"responses":{"200":{"description":"Member with the updated data","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to perform operation"}}}},"/me/notification/{id}":{"delete":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["me","notifications"],"summary":"Delete my notification","operationId":"deleteMyNotifications","parameters":[{"type":"string","description":"ID of the notification","name":"id","in":"path","required":true}],"responses":{"200":{"description":"My deleted notification","schema":{"$ref":"#/definitions/notification"}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found, or notification not found"},"417":{"description":"Unable to delete notification"}}}},"/me/notifications":{"get":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["me","notifications"],"summary":"Get member's notifications","operationId":"getMyNotifications","responses":{"200":{"description":"My notifications","schema":{"type":"array","items":{"$ref":"#/definitions/notification"}}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to get notifications"}}}},"/meetings":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Gets all meetings","operationId":"getMeetings","parameters":[{"type":"integer","format":"int64","description":"Meeting from this event","name":"event","in":"query"},{"type":"string","description":"Meeting from this team","name":"team","in":"query"},{"type":"string","description":"Meeting from this company","name":"company","in":"query"}],"responses":{"200":{"description":"Meetings filtered by query","schema":{"type":"array","items":{"$ref":"#/definitions/meeting"}}},"400":{"description":"Bad query"},"401":{"description":"Unauthorized"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Creates a meeting (must have at least coordinator credentials)","operationId":"createMeeting","parameters":[{"description":"New meeting data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["title","kind","begin","end","place"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["EVENT","TEAM","COMPANY"]},"place":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Created meeting","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Invalid payload, or couldn't create meeting"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/meetings/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Gets a meeting by id","operationId":"getMeeting","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Specified meeting","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Updates a meeting by id","operationId":"updateMeeting","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"description":"New meeting data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["title","kind","begin","end","local"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["EVENT","TEAM","COMPANY"]},"local":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Updated meeting","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Bad payload"},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Could not update meeting"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Deletes a meeting (must have at least coordinator credentials)","operationId":"deleteMeeting","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted meeting","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Meeting not found"}}}},"/meetings/{id}/minute":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["meetings"],"summary":"Upload meeting's minute by ID","operationId":"uploadMeetingMinute","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"type":"file","description":"Minute","name":"minute","in":"formData"}],"responses":{"200":{"description":"meeting with the updated data","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Invalid minute data"},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Unable to perform operation"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Delete meeting's minute by ID","operationId":"deleteMeetingMinute","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true}],"responses":{"200":{"description":"meeting with the updated data","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Unable to perform operation"}}}},"/meetings/{id}/participants":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Add a participant to a meeting","operationId":"addMeetingParticipant","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"description":"New participant data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["memberID","type"],"properties":{"memberID":{"type":"string"},"type":{"type":"string","enum":["MEMBER","COMPANYREP"]}}}}],"responses":{"200":{"description":"meeting with the updated data","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Invalid minute data"},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Unable to perform operation"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings"],"summary":"Delete a participant from a meeting","operationId":"deleteMeetingParticipant","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"description":"New participant data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["memberID","type"],"properties":{"memberID":{"type":"string"},"type":{"type":"string","enum":["MEMBER","COMPANYREP"]}}}}],"responses":{"200":{"description":"meeting with the updated data","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Unable to perform operation"}}}},"/meetings/{id}/thread":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["meetings","threads"],"summary":"Adds thread to a meeting","operationId":"addMeetingThread","parameters":[{"type":"string","description":"ID of the meeting","name":"id","in":"path","required":true},{"description":"New thread information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text","kind"],"properties":{"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated meeting","schema":{"$ref":"#/definitions/meeting"}},"400":{"description":"Invalid payload, or invalid credentials"},"401":{"description":"Unauthorized"},"404":{"description":"Meeting not found"},"417":{"description":"Unable to create post, create thread or add created thread to meeting"}}}},"/members":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get all members","operationId":"getMembers","parameters":[{"type":"string","description":"Partial and case insensitive match for name","name":"name","in":"query"},{"type":"string","description":"Members from this event","name":"event","in":"query"}],"responses":{"200":{"description":"Members filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/member"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get members"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Create a new member (must have coordinator credentials)","operationId":"createMember","parameters":[{"description":"Information needed to create the new member.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","istid"],"properties":{"istid":{"type":"string"},"name":{"type":"string"},"sinfoid":{"type":"string"}}}}],"responses":{"200":{"description":"Created member.","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid input or couldn't create the new member."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/members/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get member by ID","operationId":"getMember","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Member with the specific ID","schema":{"$ref":"#/definitions/member"}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Updates a member (must have admin credentials)","operationId":"updateMember","parameters":[{"description":"Information needed to update the member.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","istid"],"properties":{"istid":{"type":"string"},"name":{"type":"string"}}}},{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated member.","schema":{"$ref":"#/definitions/member"}},"400":{"description":"Invalid input or couldn't updated the member."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Delete a member by ID","operationId":"deleteMember","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted Member with the specific ID","schema":{"$ref":"#/definitions/member"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Member not found"},"406":{"description":"Member associated with other objects, not possible to delete"}}}},"/members/{id}/participations":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get member's participations","operationId":"getMemberParticipations","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Member with all participations","schema":{"type":"array","items":{"$ref":"#/definitions/memberParticipation"}}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to get member's credentials"}}}},"/members/{id}/role":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["members"],"summary":"Get member's role","operationId":"getMemberRole","parameters":[{"type":"string","description":"ID of the member","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Member with the specific ID","schema":{"type":"object","properties":{"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Member not found"},"417":{"description":"Unable to get member's credentials"}}}},"/packages":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Get packages","operationId":"getPackages","parameters":[{"type":"string","description":"Name of the package","name":"name","in":"query"},{"type":"integer","description":"Price of the package","name":"price","in":"query"},{"type":"integer","description":"VAT of the package","name":"vat","in":"query"}],"responses":{"200":{"description":"Packages filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/package"}}},"400":{"description":"Invalid price or vat on query"},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get packages"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Creates a new package (must have coordinator credentials)","operationId":"createPackage","parameters":[{"description":"New package data","name":"package","in":"body","required":true,"schema":{"type":"object","required":["name","items","price","vat"],"properties":{"items":{"type":"object","properties":{"item":{"type":"string"},"public":{"type":"boolean"},"quantity":{"type":"integer"}}},"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Created package","schema":{"$ref":"#/definitions/package"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Unable to create the package"}}}},"/packages/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Get package by ID","operationId":"getPackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Package with the specific ID","schema":{"$ref":"#/definitions/package"}},"401":{"description":"Unauthorized"},"404":{"description":"Package not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages"],"summary":"Updates a specific package (must have coordinator credentials)","operationId":"updatePackage","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true},{"description":"New package data","name":"package","in":"body","required":true,"schema":{"type":"object","required":["name","price","vat"],"properties":{"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}}}],"responses":{"200":{"description":"Updated package","schema":{"$ref":"#/definitions/package"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Invalid package ID"},"417":{"description":"Unable to update the package"}}}},"/packages/{id}/items":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["packages","items"],"summary":"Updates a specific package's items (must have coordinator credentials)","operationId":"updatePackageItems","parameters":[{"type":"string","description":"ID of the package","name":"id","in":"path","required":true},{"description":"New package data","name":"package","in":"body","required":true,"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"quantity":{"type":"integer"}}}}}}}],"responses":{"200":{"description":"Updated package","schema":{"$ref":"#/definitions/package"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Invalid package ID"},"417":{"description":"Unable to update the package"}}}},"/posts/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["posts"],"summary":"Get post by ID","operationId":"getPost","parameters":[{"type":"string","description":"ID of the post","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Post with the specific ID","schema":{"$ref":"#/definitions/post"}},"401":{"description":"Unauthorized"},"404":{"description":"Post not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["posts"],"summary":"Update post by ID","operationId":"updatePost","parameters":[{"type":"string","description":"ID of the post","name":"id","in":"path","required":true},{"description":"Data need for post update","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}}],"responses":{"200":{"description":"Post with the updated data","schema":{"$ref":"#/definitions/post"}},"400":{"description":"Bad payload"},"401":{"description":"Unauthorized"},"404":{"description":"Post not found"},"417":{"description":"Unable to update post"}}}},"/public/companies":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["companies","public"],"summary":"Public endpoint for getting all companies","operationId":"getCompaniesPublic","parameters":[{"type":"string","description":"Name of the company","name":"name","in":"query"},{"type":"integer","description":"ID of the event","name":"event","in":"query"},{"type":"boolean","description":"Companies participating as partner","name":"partner","in":"query"}],"responses":{"200":{"description":"Companies filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicCompany"}}},"417":{"description":"Unable to get companies"}}}},"/public/companies/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["companies","public"],"summary":"Get public company by ID","operationId":"getCompanyPublic","parameters":[{"type":"string","description":"ID of the company","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Public company with the specific ID","schema":{"$ref":"#/definitions/publicCompany"}},"401":{"description":"Unauthorized"},"404":{"description":"company not found"}}}},"/public/events":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["events","public"],"summary":"Public endpoint for getting all events","operationId":"getEventsPublic","parameters":[{"type":"boolean","description":"Get current event\nOn empty query (both current and pastEvents), returns all the events","name":"current","in":"query"},{"type":"boolean","description":"Get all the events except the current one\nOn empty query (both current and pastEvents), returns all the events","name":"pastEvents","in":"query"}],"responses":{"200":{"description":"Events filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicEvent"}}},"400":{"description":"Unable to make query"},"417":{"description":"Unable to get events"}}}},"/public/events/latest":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["events","public"],"summary":"Public endpoint for getting latest event","operationId":"getLatestEvent","responses":{"200":{"description":"Latest event","schema":{"$ref":"#/definitions/event"}},"417":{"description":"Unable to get event"}}}},"/public/members":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["members","public"],"summary":"Get all members, based on query","operationId":"getMembersPublic","parameters":[{"type":"string","description":"Name of the member","name":"name","in":"query"},{"type":"integer","format":"int64","description":"Member from this event","name":"event","in":"query"}],"responses":{"200":{"description":"Members filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicMember"}}},"417":{"description":"Unable to get members"}}}},"/public/sessions":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["sessions","public"],"summary":"Get all sessions, based on query","operationId":"getSessionsPublic","parameters":[{"type":"integer","format":"int64","description":"Session from this event","name":"event","in":"query"},{"enum":["TALK","PRESENTATION","WORKSHOP"],"type":"string","description":"Kind of session","name":"kind","in":"query"}],"responses":{"200":{"description":"Sessions filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicSession"}}},"417":{"description":"Unable to get sessions"}}}},"/public/sessions/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions","public"],"summary":"Get public session by ID","operationId":"getSessionPublic","parameters":[{"type":"string","description":"ID of the session","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Public session with the specific ID","schema":{"$ref":"#/definitions/publicSession"}},"401":{"description":"Unauthorized"},"404":{"description":"session not found"}}}},"/public/speakers":{"get":{"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","public"],"summary":"Public endpoint for getting all speakers","operationId":"getSpeakersPublic","parameters":[{"type":"string","description":"Name of the speaker","name":"name","in":"query"},{"type":"integer","description":"ID of the event","name":"event","in":"query"}],"responses":{"200":{"description":"Speakers filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/publicSpeaker"}}},"417":{"description":"Unable to get speakers"}}}},"/public/speakers/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","public"],"summary":"Get public speaker by ID","operationId":"getSpeakerPublic","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Public speaker with the specific ID","schema":{"$ref":"#/definitions/publicSpeaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}}},"/sessions":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Get sessions","operationId":"getSessions","parameters":[{"type":"integer","format":"int64","description":"Session from this event","name":"event","in":"query"},{"type":"string","format":"date-time","description":"Session began before this date","name":"before","in":"query"},{"type":"string","format":"date-time","description":"Session ended after this date","name":"after","in":"query"},{"type":"string","description":"Session happened on this location inside the venue","name":"place","in":"query"},{"enum":["TALK","PRESENTATION","WORKSHOP"],"type":"string","description":"Kind of session","name":"kind","in":"query"},{"type":"string","description":"Session given by this company","name":"company","in":"query"},{"type":"string","description":"Session given by this speaker","name":"speaker","in":"query"}],"responses":{"200":{"description":"Sessions filtered by query","schema":{"type":"array","items":{"$ref":"#/definitions/session"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to make query"}}}},"/sessions/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Get session by ID","operationId":"getSession","parameters":[{"type":"string","description":"ID of the session","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Session with the specific ID","schema":{"$ref":"#/definitions/session"}},"401":{"description":"Unauthorized"},"404":{"description":"Session not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Updates session (must have coordinator credentials)","operationId":"updateSession","parameters":[{"type":"string","description":"ID of the session","name":"id","in":"path","required":true},{"description":"New session data\n - kind: TALK, PRESENTATION or WORKSHOP\n - if kind=TALK, then speaker must be specified\n - if kind=PRESENTATION or kind=WORKSHOP, then company must be specified\n - place is optional\n - videoURL is optional","name":"session","in":"body","required":true,"schema":{"type":"object","required":["begin","end","title","description","kind"],"properties":{"begin":{"type":"string","format":"date-time"},"company":{"type":"string"},"description":{"type":"string"},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"type":"string"},"tickets":{"type":"object","properties":{"end":{"type":"string","format":"date-time"},"max":{"type":"integer"},"start":{"type":"string","format":"date-time"}}},"title":{"type":"string"},"videoURL":{"type":"string"}}}}],"responses":{"200":{"description":"Updated session","schema":{"$ref":"#/definitions/session"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Session not found"},"417":{"description":"Unable to update session"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["sessions"],"summary":"Delete a specific session","operationId":"deleteSession","parameters":[{"type":"string","description":"Id of the session to be deleted.","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Session deleted successfully","schema":{"type":"array","items":{"$ref":"#/definitions/session"}}},"401":{"description":"Unauthorized"},"404":{"description":"Session not found."}}}},"/speakers":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Get all speakers","operationId":"getSpeakers","parameters":[{"type":"string","description":"Name of the speaker","name":"name","in":"query"},{"type":"integer","description":"Has a participation entry for this event","name":"event","in":"query"},{"type":"string","description":"Was contacted by this member","name":"member","in":"query"}],"responses":{"200":{"description":"Speakers filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/speaker"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get speakers"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Create a new speaker","operationId":"createSpeaker","parameters":[{"description":"Information needed to create the new speaker.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","title","bio"],"properties":{"bio":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Created speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid input or couldn't create the new speaker"},"401":{"description":"Unauthorized"}}}},"/speakers/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Get speaker by ID","operationId":"getSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Speaker with the specific ID","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker by ID","operationId":"updateSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"Information needed to update the speaker.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name","bio","title","notes"],"properties":{"bio":{"type":"string"},"name":{"type":"string"},"notes":{"type":"string"},"title":{"type":"string"}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Delete a specific speaker","operationId":"deleteSpeaker","parameters":[{"type":"string","description":"Id of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"The deleted speaker","schema":{"type":"array","items":{"$ref":"#/definitions/speaker"}}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}}},"/speakers/{id}/image/internal":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's internal image by ID","operationId":"updateSpeakerInternalImage","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"file","description":"Photo of speaker","name":"image","in":"formData"}],"responses":{"200":{"description":"Speaker with the updated data","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to perform operation"}}}},"/speakers/{id}/image/public/company":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's company public image by ID (must have at least coordinator credentials)","operationId":"updateSpeakerCompanyImage","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"file","description":"Logo of speaker's company","name":"image","in":"formData"}],"responses":{"200":{"description":"Speaker with the updated data","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to perform operation"}}}},"/speakers/{id}/image/public/speaker":{"post":{"security":[{"Bearer":[]}],"consumes":["multipart/form-data"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's public image by ID (must have at least coordinator credentials)","operationId":"updateSpeakerPublicImage","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"file","description":"Photo of speaker","name":"image","in":"formData"}],"responses":{"200":{"description":"Speaker with the updated data","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid image data"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to perform operation"}}}},"/speakers/{id}/participation":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Update speaker's participation on the current event","operationId":"updateSpeakerParticipation","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"New participation information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","feedback","room"],"properties":{"feedback":{"type":"string"},"member":{"type":"string"},"room":{"type":"object","properties":{"cost":{"type":"integer"},"notes":{"type":"string"},"type":{"type":"string"}}}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker's participation data"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Adds participation on the current event to a speaker","operationId":"addSpeakerParticipation","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Unable to add participation for the current event to this speaker"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Removes a speakers' participation from the current event. Admin only and must have no communications and not be associated with any session ","operationId":"removeSpeakerParticipation","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to remove speaker's participation: Has communications or is associated with a session"}}}},"/speakers/{id}/participation/flightInfo":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","flightInfo"],"summary":"Adds flightInfo to a speaker's participation","operationId":"addSpeakerFlightInfo","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"Information needed to create a flight info.\n - Inbound/outbound: airports\n - Link: URL to the flight company's flight\n - Cost: in cents (€)","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["inbound","outbound","from","to","bought","cost","notes"],"properties":{"bought":{"type":"boolean"},"cost":{"description":"In cents (€)","type":"integer"},"from":{"description":"Airport","type":"string"},"inbound":{"type":"string","format":"date-time"},"link":{"description":"URL to the flight","type":"string"},"notes":{"type":"string"},"outbound":{"type":"string","format":"date-time"},"to":{"description":"Airport","type":"string"}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to create flight info, or add it to the speaker's participation"}}}},"/speakers/{id}/participation/flightInfo/{flightInfoID}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","flightInfo"],"summary":"Removes flightInfo from a speaker's participation, and deletes it from the database (must have at least coordinator credentials)","operationId":"removeSpeakerFlightInfo","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"string","description":"ID of the flightInfo","name":"flightInfoID","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to remove or delete flight info"}}}},"/speakers/{id}/participation/status/next":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Gets all the valid steps to be taken on a speaker's participation status on the current event","operationId":"getvalidSpeakerParticipationStatusSteps","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Valid steps","schema":{"type":"object","properties":{"steps":{"type":"array","items":{"type":"object","properties":{"next":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"step":{"type":"integer"}}}}}}},"400":{"description":"Speaker without participation on the current event"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"}}}},"/speakers/{id}/participation/status/{status}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Update a speaker's participation status on the current event (admin credentials)","operationId":"updateSpeakerParticipationStatus","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"],"type":"string","description":"New status","name":"status","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker's participation status"}}}},"/speakers/{id}/participation/status/{step}":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Step a speaker's participation status on the current event","operationId":"stepSpeakerParticipationStatus","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"type":"integer","description":"Step to the next status","name":"step","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to update speaker's participation status"}}}},"/speakers/{id}/subscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Subscribe to speaker by ID","operationId":"subscribeToSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to subscribe to speaker"}}}},"/speakers/{id}/thread":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers","threads"],"summary":"Adds thread on the current event to a speaker","operationId":"addSpeakerThread","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true},{"description":"New thread information","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text","kind"],"properties":{"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"meeting":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"participants":{"type":"object","properties":{"companyReps":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"string"}}}},"place":{"type":"string"}}},"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"400":{"description":"Invalid payload, or invalid credentials"},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to create post, create thread or add created thread to speaker participation"}}}},"/speakers/{id}/unsubscribe":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["speakers"],"summary":"Unsubscribe to speaker by ID","operationId":"unsubscribeToSpeaker","parameters":[{"type":"string","description":"ID of the speaker","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated speaker","schema":{"$ref":"#/definitions/speaker"}},"401":{"description":"Unauthorized"},"404":{"description":"Speaker not found"},"417":{"description":"Unable to unsubscribe from speaker"}}}},"/teams":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Get all teams","operationId":"getTeams","parameters":[{"type":"string","description":"Name of the team","name":"name","in":"query"},{"type":"string","description":"Contains this member","name":"member","in":"query"},{"type":"string","description":"Contains all the members whose name match the given on this query","name":"memberName","in":"query"},{"type":"integer","format":"int64","description":"Team from this event","name":"event","in":"query"}],"responses":{"200":{"description":"Teams filtered by the query","schema":{"type":"array","items":{"$ref":"#/definitions/team"}}},"401":{"description":"Unauthorized"},"417":{"description":"Unable to get teams"}}},"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Create a new team (must have coordinator credentials). Created teams are added to the current event","operationId":"createTeam","parameters":[{"description":"Information needed to create the new team.","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}],"responses":{"200":{"description":"Created team.","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Invalid input or couldn't create the new team."},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"}}}},"/teams/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Get team by ID","operationId":"getTeam","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Team with the specific ID","schema":{"$ref":"#/definitions/team"}},"401":{"description":"Unauthorized"},"404":{"description":"Team not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Updates a teams's name (must have coordinator credentials)","operationId":"updateTeam","parameters":[{"description":"New team data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}},{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated team","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Invalid payload, or couldn't update team"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"417":{"description":"Couldn't find team"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams"],"summary":"Deletes a team (must have admin credentials)","operationId":"deleteTeam","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Deleted team","schema":{"$ref":"#/definitions/team"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team not found"}}}},"/teams/{id}/meetings":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","meetings"],"summary":"Creates a meeting and adds it to a team","operationId":"addTeamMeeting","parameters":[{"description":"New meeting data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["title","kind","begin","end","local"],"properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["EVENT","TEAM","COMPANY"]},"local":{"type":"string"},"title":{"type":"string"}}}},{"type":"string","description":"ID of the team","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Updated team","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Invalid payload, or couldn't create meeting"},"401":{"description":"Unauthorized"},"417":{"description":"Couldn't find team"}}}},"/teams/{id}/meetings/{meetingID}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","meetings"],"summary":"Removes a meeting from a team (must have at least Team Leader credentials)","operationId":"deleteTeamMeeting","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"type":"string","description":"ID of the meeting","name":"meetingID","in":"path","required":true}],"responses":{"200":{"description":"Removed meeting","schema":{"$ref":"#/definitions/meeting"}},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Couldn't find team or meeting, or meeting not on team"},"417":{"description":"Couldn't find team"}}}},"/teams/{id}/members":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","members"],"summary":"Add a new member to the team (must have coordinator credentials)","operationId":"addTeamMember","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"description":"New member data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","role"],"properties":{"member":{"type":"string"},"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]}}}}],"responses":{"200":{"description":"Team with the added member","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Bad name or role on payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team or member not found"}}}},"/teams/{id}/members/{memberID}":{"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","members"],"summary":"Updates a member's role on the team (must have coordinator credentials)","operationId":"updateTeamMemberRole","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"type":"string","description":"ID of the member","name":"memberID","in":"path","required":true},{"description":"New member data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["member","role"],"properties":{"role":{"type":"string"}}}}],"responses":{"200":{"description":"Team with the updated member","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Bad name or role on payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team or member not found"}}},"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["teams","members"],"summary":"Removes a member from the team (must have coordinator credentials)","operationId":"deleteTeamMember","parameters":[{"type":"string","description":"ID of the team","name":"id","in":"path","required":true},{"type":"string","description":"ID of the member","name":"memberID","in":"path","required":true}],"responses":{"200":{"description":"Team without the removed member","schema":{"$ref":"#/definitions/team"}},"400":{"description":"Bad name or role on payload"},"401":{"description":"Unauthorized"},"403":{"description":"Valid authorization, but not enough credentials"},"404":{"description":"Team or member not found"}}}},"/threads/{id}":{"get":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads"],"summary":"Get thread by ID","operationId":"getThread","parameters":[{"type":"string","description":"ID of the thread","name":"id","in":"path","required":true}],"responses":{"200":{"description":"Thread with the specific ID","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"404":{"description":"Thread not found"}}},"put":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads"],"summary":"Updates a thread. Only valid if you own the thread (or admin)","operationId":"updateThread","parameters":[{"type":"string","description":"ID of the thread","name":"id","in":"path","required":true},{"description":"Update data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["kind","meeting"],"properties":{"kind":{"type":"string"},"meeting":{"type":"string"}}}}],"responses":{"200":{"description":"Updated thread","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"403":{"description":"Authtenticated, but access level is not enough"},"404":{"description":"Thread not found"}}}},"/threads/{id}/comments":{"post":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads","posts"],"summary":"Add comment to thread","operationId":"addCommentToThread","parameters":[{"type":"string","description":"ID of the thread","name":"id","in":"path","required":true},{"description":"Comment data","name":"payload","in":"body","required":true,"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}}],"responses":{"200":{"description":"Updated Thread","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"417":{"description":"Thread not found, or unable to create and/or add post to thread"}}}},"/threads/{threadID}/comments/{postID}":{"delete":{"security":[{"Bearer":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["threads","posts"],"summary":"Remove comment from thread","operationId":"removeCommentFromThread","parameters":[{"type":"string","description":"ID of the thread","name":"threadID","in":"path","required":true},{"type":"string","description":"ID of the post","name":"postID","in":"path","required":true}],"responses":{"200":{"description":"Updated Thread","schema":{"$ref":"#/definitions/thread"}},"401":{"description":"Unauthorized"},"404":{"description":"Thread or post not found"},"417":{"description":"Unable to remove post from thread"}}}}},"definitions":{"billing":{"type":"object","properties":{"company":{"type":"string"},"emission":{"type":"string","format":"date-time"},"event":{"type":"integer"},"id":{"type":"string"},"invoiceNumber":{"type":"integer"},"notes":{"type":"string"},"status":{"type":"object","properties":{"invoice":{"type":"boolean"},"paid":{"type":"boolean"},"proForma":{"type":"boolean"},"receipt":{"type":"boolean"}}},"value":{"type":"integer"},"visible":{"type":"boolean"}}},"company":{"type":"object","properties":{"billingInfo":{"type":"object","properties":{"address":{"type":"string"},"name":{"type":"string"},"tin":{"type":"string"}}},"description":{"type":"string"},"employers":{"type":"array","items":{"type":"string"}},"id":{"type":"string"},"imgs":{"type":"object","properties":{"internal":{"type":"string"},"public":{"type":"string"}}},"name":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"billing":{"type":"string"},"communications":{"type":"array","items":{"type":"string"}},"confirmed":{"type":"string","format":"date-time"},"event":{"type":"integer","format":"int64"},"member":{"type":"string"},"notes":{"type":"string"},"package":{"type":"string"},"partner":{"type":"boolean"},"status":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"subscribers":{"type":"array","items":{"type":"string"}}}}},"site":{"type":"string"}}},"companyRep":{"type":"object","properties":{"contact":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}},"contact":{"type":"object","properties":{"id":{"type":"string"},"mails":{"type":"array","items":{"type":"object","properties":{"mail":{"type":"string"},"personal":{"type":"boolean"},"valid":{"type":"boolean"}}}},"phones":{"type":"array","items":{"type":"object","properties":{"phone":{"type":"string"},"valid":{"type":"boolean"}}}},"socials":{"type":"object","properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"event":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"id":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"type":"string"}},"meetings":{"type":"array","items":{"type":"string"}},"name":{"type":"string"},"packages":{"type":"array","items":{"type":"object","properties":{"available":{"type":"boolean"},"public_name":{"type":"string"},"template":{"type":"string"}}}},"sessions":{"type":"array","items":{"type":"string"}},"teams":{"type":"array","items":{"type":"string"}},"themes":{"type":"array","items":{"type":"string"}}}},"flightInfo":{"type":"object","properties":{"bought":{"type":"boolean"},"cost":{"type":"integer"},"from":{"type":"string"},"id":{"type":"string"},"inbound":{"type":"string","format":"date-time"},"link":{"type":"string"},"notes":{"type":"string"},"outbound":{"type":"string","format":"date-time"},"to":{"type":"string"}}},"item":{"type":"object","properties":{"description":{"type":"string"},"id":{"type":"string"},"img":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer"},"type":{"type":"string"},"vat":{"type":"integer"}}},"meeting":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"communications":{"type":"array","items":{"type":"string"}},"end":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["EVENT","TEAM","COMPANY"]},"minute":{"type":"string"},"participants":{"type":"object","properties":{"companyReps":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"string"}}}},"place":{"type":"string"},"title":{"type":"string"}}},"member":{"type":"object","properties":{"contact":{"type":"string"},"id":{"type":"string"},"img":{"type":"string"},"istid":{"type":"string"},"name":{"type":"string"},"sinfoid":{"type":"string"}}},"memberParticipation":{"type":"object","properties":{"event":{"type":"integer"},"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]},"team":{"type":"string"}}},"notification":{"type":"object","properties":{"company":{"type":"string"},"date":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["CREATED","UPDATED","TAGGED","DELETED"]},"meeting":{"type":"string"},"member":{"type":"string"},"post":{"type":"string"},"session":{"type":"string"},"signature":{"type":"string"},"speaker":{"type":"string"}}},"package":{"type":"object","properties":{"id":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"public":{"type":"boolean"},"quantity":{"type":"integer"}}}},"name":{"type":"string"},"price":{"type":"integer"},"vat":{"type":"integer"}}},"post":{"type":"object","properties":{"id":{"type":"string"},"member":{"type":"string"},"posted":{"type":"string","format":"date-time"},"text":{"type":"string"},"updated":{"type":"string","format":"date-time"}}},"publicCompany":{"type":"object","properties":{"id":{"type":"string"},"img":{"type":"string"},"name":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"event":{"type":"integer"},"package":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"quantity":{"type":"integer"}}}},"name":{"type":"string"}}},"partner":{"type":"boolean"}}}},"site":{"type":"string"}}},"publicEvent":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"themes":{"type":"array","items":{"type":"string"}}}},"publicMember":{"type":"object","properties":{"img":{"type":"string"},"name":{"type":"string"},"socials":{"type":"object","properties":{"facebook":{"type":"string"},"github":{"type":"string"},"linkedin":{"type":"string"},"skype":{"type":"string"},"twitter":{"type":"string"}}}}},"publicSession":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"company":{"$ref":"#/definitions/publicCompany"},"description":{"type":"string"},"end":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"$ref":"#/definitions/publicSpeaker"},"tickets":{"type":"object","properties":{"end":{"type":"string","format":"date-time"},"max":{"type":"integer"},"start":{"type":"string","format":"date-time"}}},"title":{"type":"string"},"videoURL":{"type":"string"}}},"publicSpeaker":{"type":"object","properties":{"id":{"type":"string"},"imgs":{"type":"object","properties":{"company":{"type":"string"},"speaker":{"type":"string"}}},"name":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"event":{"type":"integer"},"feedback":{"type":"string"}}}},"title":{"type":"string"}}},"session":{"type":"object","properties":{"begin":{"type":"string","format":"date-time"},"company":{"type":"string"},"description":{"type":"string"},"end":{"type":"string","format":"date-time"},"id":{"type":"string"},"kind":{"type":"string","enum":["TALK","PRESENTATION","WORKSHOP"]},"place":{"type":"string"},"speaker":{"type":"string"},"tickets":{"type":"object","properties":{"end":{"type":"string","format":"date-time"},"max":{"type":"integer"},"start":{"type":"string","format":"date-time"}}},"title":{"type":"string"},"videoURL":{"type":"string"}}},"speaker":{"type":"object","properties":{"bio":{"type":"string"},"contact":{"type":"string"},"id":{"type":"string"},"imgs":{"type":"object","properties":{"company":{"type":"string"},"internal":{"type":"string"},"speaker":{"type":"string"}}},"name":{"type":"string"},"notes":{"type":"string"},"participations":{"type":"array","items":{"type":"object","properties":{"communications":{"type":"array","items":{"type":"string"}},"event":{"type":"integer"},"feedback":{"type":"string"},"flights":{"type":"array","items":{"type":"string"}},"member":{"type":"string"},"room":{"type":"object","properties":{"cost":{"type":"integer"},"notes":{"type":"string"},"type":{"type":"string"}}},"status":{"type":"string","enum":["SUGGESTED","SELECTED","ON_HOLD","CONTACTED","IN_CONVERSATIONS","ACCEPTED","REJECTED","GIVEN_UP","ANNOUNCED"]},"subscribers":{"type":"array","items":{"type":"string"}}}}},"title":{"type":"string"}}},"team":{"type":"object","properties":{"id":{"type":"string"},"meetings":{"type":"array","items":{"type":"string"}},"members":{"type":"array","items":{"type":"object","properties":{"member":{"type":"string"},"role":{"type":"string","enum":["MEMBER","TEAMLEADER","COORDINATOR","ADMIN"]}}}},"name":{"type":"string"}}},"thread":{"type":"object","properties":{"comments":{"type":"array","items":{"type":"string"}},"entry":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string","enum":["TEMPLATE","TO","FROM","MEETING","PHONE_CALL"]},"meeting":{"type":"string"},"posted":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["APPROVED","REVIEWED","PENDING"]}}}},"securityDefinitions":{"Bearer":{"type":"apiKey","name":"Authorization","in":"header"}},"tags":[{"description":"Public endpoints (don't need authentication)","name":"public"},{"description":"Authentication endpoints","name":"auth"},{"description":"Saved bills","name":"billings"},{"description":"Contacted companies","name":"companies"},{"description":"Person representing a certain company","name":"companyReps"},{"description":"All of our saved contacts","name":"contacts"},{"description":"Information relative to each event","name":"events"},{"description":"Saved flight information","name":"flightInfo"},{"description":"Items in packages sold by SINFO","name":"items"},{"description":"Saved meetings' information","name":"meetings"},{"description":"Member's personal endpoints","name":"me"},{"description":"Information related to each SINFO's member","name":"members"},{"description":"Notifications for our members to keep up with updated information","name":"notifications"},{"description":"Packages sold/traded by SINFO to companies","name":"packages"},{"description":"Messages shared by SINFO's members","name":"posts"},{"description":"Scheduled sessions for the event's week, such as keynotes, presentations, etc","name":"sessions"},{"description":"Speakers for the event's keynotes","name":"speakers"},{"description":"SINFO's teams","name":"teams"},{"description":"Additional communication taken inside the posts","name":"threads"}],"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"}} \ No newline at end of file diff --git a/backend/swagger/events-meetings.json b/backend/swagger/events-meetings.json index 72dba2d7..c8bd9741 100644 --- a/backend/swagger/events-meetings.json +++ b/backend/swagger/events-meetings.json @@ -26,11 +26,24 @@ "schema": { "type": "object", "required": [ + "title", + "kind", "begin", "end", "place" ], "properties": { + "title": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "EVENT", + "TEAM", + "COMPANY" + ] + }, "begin": { "type": "string", "format": "date-time" diff --git a/backend/swagger/meetings-id-minute.json b/backend/swagger/meetings-id-minute.json index c7dc1a17..ba579940 100644 --- a/backend/swagger/meetings-id-minute.json +++ b/backend/swagger/meetings-id-minute.json @@ -51,5 +51,49 @@ "description": "Invalid minute data" } } + }, + "delete": { + "tags": [ + "meetings" + ], + "summary": "Delete meeting's minute by ID", + "operationId": "deleteMeetingMinute", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "ID of the meeting", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "meeting with the updated data", + "schema": { + "$ref": "./models/meeting.json" + } + }, + "417": { + "description": "Unable to perform operation" + }, + "404": { + "description": "Meeting not found" + }, + "401": { + "description": "Unauthorized" + } + } } } \ No newline at end of file diff --git a/backend/swagger/meetings-id-participants.json b/backend/swagger/meetings-id-participants.json new file mode 100644 index 00000000..12c3f186 --- /dev/null +++ b/backend/swagger/meetings-id-participants.json @@ -0,0 +1,143 @@ +{ + "post": { + "tags": [ + "meetings" + ], + "summary": "Add a participant to a meeting", + "operationId": "addMeetingParticipant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "ID of the meeting", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "payload", + "description": "New participant data", + "required": true, + "schema": { + "type": "object", + "required": [ + "memberID", + "type" + ], + "properties": { + "memberID": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "MEMBER", + "COMPANYREP" + ] + } + } + } + } + ], + "responses": { + "200": { + "description": "meeting with the updated data", + "schema": { + "$ref": "./models/meeting.json" + } + }, + "417": { + "description": "Unable to perform operation" + }, + "404": { + "description": "Meeting not found" + }, + "401": { + "description": "Unauthorized" + }, + "400": { + "description": "Invalid minute data" + } + } + }, + "delete": { + "tags": [ + "meetings" + ], + "summary": "Delete a participant from a meeting", + "operationId": "deleteMeetingParticipant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "ID of the meeting", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "payload", + "description": "New participant data", + "required": true, + "schema": { + "type": "object", + "required": [ + "memberID", + "type" + ], + "properties": { + "memberID": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "MEMBER", + "COMPANYREP" + ] + } + } + } + } + ], + "responses": { + "200": { + "description": "meeting with the updated data", + "schema": { + "$ref": "./models/meeting.json" + } + }, + "417": { + "description": "Unable to perform operation" + }, + "404": { + "description": "Meeting not found" + }, + "401": { + "description": "Unauthorized" + } + } + } +} \ No newline at end of file diff --git a/backend/swagger/meetings-id-thread.json b/backend/swagger/meetings-id-thread.json new file mode 100644 index 00000000..d111386b --- /dev/null +++ b/backend/swagger/meetings-id-thread.json @@ -0,0 +1,78 @@ +{ + "post": { + "tags": [ + "meetings", + "threads" + ], + "summary": "Adds thread to a meeting", + "operationId": "addMeetingThread", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "ID of the meeting", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "payload", + "description": "New thread information", + "required": true, + "schema": { + "type": "object", + "required": [ + "text", + "kind" + ], + "properties": { + "text": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "TEMPLATE", + "TO", + "FROM", + "MEETING", + "PHONE_CALL" + ] + } + } + } + } + ], + "responses": { + "200": { + "description": "Updated meeting", + "schema": { + "$ref": "./models/meeting.json" + } + }, + "400": { + "description": "Invalid payload, or invalid credentials" + }, + "404": { + "description": "Meeting not found" + }, + "401": { + "description": "Unauthorized" + }, + "417": { + "description": "Unable to create post, create thread or add created thread to meeting" + } + } + } +} diff --git a/backend/swagger/meetings-id.json b/backend/swagger/meetings-id.json index 18819e4a..3202e6bd 100644 --- a/backend/swagger/meetings-id.json +++ b/backend/swagger/meetings-id.json @@ -117,11 +117,24 @@ "schema": { "type": "object", "required": [ + "title", + "kind", "begin", "end", "local" ], "properties": { + "title": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "EVENT", + "TEAM", + "COMPANY" + ] + }, "begin": { "type": "string", "format": "date-time" @@ -132,9 +145,6 @@ }, "local": { "type": "string" - }, - "minute": { - "type": "string" } } } diff --git a/backend/swagger/meetings.json b/backend/swagger/meetings.json index 7a13e01c..7ee18945 100644 --- a/backend/swagger/meetings.json +++ b/backend/swagger/meetings.json @@ -25,11 +25,24 @@ "schema": { "type": "object", "required": [ + "title", + "kind", "begin", "end", - "local" + "place" ], "properties": { + "title": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "EVENT", + "TEAM", + "COMPANY" + ] + }, "begin": { "type": "string", "format": "date-time" @@ -38,7 +51,7 @@ "type": "string", "format": "date-time" }, - "local": { + "place": { "type": "string" } } diff --git a/backend/swagger/models/meeting.json b/backend/swagger/models/meeting.json index 81d2ab31..db084ba8 100644 --- a/backend/swagger/models/meeting.json +++ b/backend/swagger/models/meeting.json @@ -12,12 +12,29 @@ "type": "string", "format": "date-time" }, + "title": { + "type": "string" + }, "place": { "type": "string" }, "minute": { "type": "string" }, + "kind": { + "type": "string", + "enum": [ + "EVENT", + "TEAM", + "COMPANY" + ] + }, + "communications": { + "type": "array", + "items": { + "type": "string" + } + }, "participants": { "type": "object", "properties": { diff --git a/backend/swagger/sessions-id.json b/backend/swagger/sessions-id.json index 515ee8f6..129e7beb 100644 --- a/backend/swagger/sessions-id.json +++ b/backend/swagger/sessions-id.json @@ -157,5 +157,49 @@ "description": "Valid authorization, but not enough credentials" } } + }, + "delete": { + "tags": [ + "sessions" + ], + "summary": "Delete a specific session", + "operationId": "deleteSession", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Id of the session to be deleted.", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Session deleted successfully", + "schema": { + "type": "array", + "items": { + "$ref": "./models/session.json" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Session not found." + } + } } } \ No newline at end of file diff --git a/backend/swagger/speakers-id.json b/backend/swagger/speakers-id.json index becd1abd..43369d1c 100644 --- a/backend/swagger/speakers-id.json +++ b/backend/swagger/speakers-id.json @@ -115,5 +115,49 @@ "description": "Unable to update speaker" } } + }, + "delete": { + "tags": [ + "speakers" + ], + "summary": "Delete a specific speaker", + "operationId": "deleteSpeaker", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Id of the speaker", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The deleted speaker", + "schema": { + "type": "array", + "items": { + "$ref": "./models/speaker.json" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Speaker not found" + } + } } } \ No newline at end of file diff --git a/backend/swagger/swagger.json b/backend/swagger/swagger.json index 3c355657..2a0dbcb7 100644 --- a/backend/swagger/swagger.json +++ b/backend/swagger/swagger.json @@ -362,6 +362,12 @@ }, "/meetings/{id}/minute": { "$ref": "./meetings-id-minute.json" + }, + "/meetings/{id}/thread": { + "$ref": "./meetings-id-thread.json" + }, + "/meetings/{id}/participants": { + "$ref": "./meetings-id-participants.json" } }, "externalDocs": { diff --git a/backend/swagger/teams-id-meetings.json b/backend/swagger/teams-id-meetings.json index 93b219ea..89493d96 100644 --- a/backend/swagger/teams-id-meetings.json +++ b/backend/swagger/teams-id-meetings.json @@ -26,11 +26,24 @@ "schema": { "type": "object", "required": [ + "title", + "kind", "begin", "end", "local" ], "properties": { + "title": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "EVENT", + "TEAM", + "COMPANY" + ] + }, "begin": { "type": "string", "format": "date-time" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index bb1d0203..da565d02 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -11,7 +11,7 @@ RUN git clone https://github.com/flutter/flutter.git /usr/local/flutter ENV PATH="/usr/local/flutter/bin:/usr/local/flutter/bin/cache/dart-sdk/bin:${PATH}" # Enable flutter web -RUN flutter channel master +RUN flutter channel stable RUN flutter upgrade RUN flutter config --enable-web diff --git a/frontend/lib/components/addThreadForm.dart b/frontend/lib/components/addThreadForm.dart index dd7a1ab3..692f8656 100644 --- a/frontend/lib/components/addThreadForm.dart +++ b/frontend/lib/components/addThreadForm.dart @@ -1,28 +1,26 @@ -import 'dart:io'; - -import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_dropzone/flutter_dropzone.dart'; -import 'package:frontend/components/appbar.dart'; import 'package:frontend/models/company.dart'; +import 'package:frontend/models/meeting.dart'; import 'package:frontend/models/speaker.dart'; import 'package:frontend/services/companyService.dart'; +import 'package:frontend/services/meetingService.dart'; import 'package:frontend/services/speakerService.dart'; -import 'package:image_picker/image_picker.dart'; class AddThreadForm extends StatefulWidget { final Speaker? speaker; final Company? company; + final Meeting? meeting; final void Function(BuildContext, Speaker?)? onEditSpeaker; final void Function(BuildContext, Company?)? onEditCompany; + final void Function(BuildContext, Meeting?)? onEditMeeting; AddThreadForm( {Key? key, this.speaker, this.company, + this.meeting, this.onEditSpeaker, - this.onEditCompany}) + this.onEditCompany, + this.onEditMeeting}) : super(key: key); @override @@ -82,6 +80,31 @@ class _AddThreadFormState extends State { const SnackBar(content: Text('An error occured.')), ); } + } else if (widget.meeting != null && widget.onEditMeeting != null) { + MeetingService service = MeetingService(); + Meeting? m = await service.addThread( + id: widget.meeting!.id, kind: 'MEETING', text: text); + if (m != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + widget.onEditMeeting!(context, m); + + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + + Navigator.pop(context); + } } } } @@ -111,30 +134,31 @@ class _AddThreadFormState extends State { ), ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: DropdownButtonFormField( - icon: Icon(Icons.tag), - items: kinds - .map( - (e) => DropdownMenuItem(value: e, child: Text(e))) - .toList(), - value: kinds[0], - selectedItemBuilder: (BuildContext context) { - return kinds.map((e) { - return Align( - alignment: AlignmentDirectional.centerStart, - child: Container(child: Text(e)), - ); - }).toList(); - }, - onChanged: (next) { - setState(() { - kind = next!; - }); - }, + if (widget.meeting == null && widget.onEditMeeting == null) + Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButtonFormField( + icon: Icon(Icons.tag), + items: kinds + .map((e) => + DropdownMenuItem(value: e, child: Text(e))) + .toList(), + value: kinds[0], + selectedItemBuilder: (BuildContext context) { + return kinds.map((e) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Container(child: Text(e)), + ); + }).toList(); + }, + onChanged: (next) { + setState(() { + kind = next!; + }); + }, + ), ), - ), Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( diff --git a/frontend/lib/components/appbar.dart b/frontend/lib/components/appbar.dart index c53c944c..e8d5230c 100644 --- a/frontend/lib/components/appbar.dart +++ b/frontend/lib/components/appbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:frontend/components/SearchResultWidget.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/components/SearchResultWidget.dart'; @@ -72,6 +73,8 @@ class _CustomAppBarState extends State { int current = notifier.event.id; return Column(children: [ AppBar( + backgroundColor: Theme.of(context).primaryColor, + iconTheme: IconThemeData(color: Colors.white), actions: actions, title: Row(children: [ InkWell( diff --git a/frontend/lib/components/blurryDialog.dart b/frontend/lib/components/blurryDialog.dart index 8e5f559c..abc21584 100644 --- a/frontend/lib/components/blurryDialog.dart +++ b/frontend/lib/components/blurryDialog.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class BlurryDialog extends StatelessWidget { diff --git a/frontend/lib/components/deckTheme.dart b/frontend/lib/components/deckTheme.dart index 8564fac8..b88ffdd4 100644 --- a/frontend/lib/components/deckTheme.dart +++ b/frontend/lib/components/deckTheme.dart @@ -18,11 +18,13 @@ class LightTheme extends BaseTheme { tabBarTheme: TabBarTheme(labelColor: Colors.black), primarySwatch: Colors.indigo, primaryColor: Colors.indigo, - brightness: Brightness.light, backgroundColor: const Color(0xFFE5E5E5), - accentColor: Color.fromRGBO(92, 127, 242, 1), + //secondary: Color.fromRGBO(92, 127, 242, 1) + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.indigo) + .copyWith(secondary: Colors.grey, + brightness: Brightness.light,), cardColor: Color.fromRGBO(241, 241, 241, 1), - accentIconTheme: IconThemeData(color: Colors.white), + iconTheme: IconThemeData(color: Colors.black), dividerColor: Colors.grey, disabledColor: Colors.grey, ); @@ -34,16 +36,17 @@ class LightTheme extends BaseTheme { class DarkTheme extends BaseTheme { ThemeData get materialTheme { return ThemeData( - tabBarTheme: TabBarTheme(labelColor: Colors.white), + tabBarTheme: TabBarTheme(labelColor: Colors.white,), disabledColor: Colors.grey, primarySwatch: Colors.grey, primaryColor: Colors.black, - brightness: Brightness.dark, backgroundColor: Colors.white, - accentColor: Colors.white, + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.grey) + .copyWith(secondary: Colors.white, + brightness: Brightness.dark,), cardColor: Color.fromRGBO(0, 0, 0, 0.6), - accentIconTheme: IconThemeData(color: Colors.black), - dividerColor: Colors.black12, + iconTheme: IconThemeData(color: Colors.white), + dividerColor: Colors.grey, ); } diff --git a/frontend/lib/components/memberPartCard.dart b/frontend/lib/components/memberPartCard.dart index 8a34308c..676de659 100644 --- a/frontend/lib/components/memberPartCard.dart +++ b/frontend/lib/components/memberPartCard.dart @@ -1,4 +1,8 @@ +import 'dart:html'; + import 'package:flutter/material.dart'; +import 'package:frontend/services/authService.dart'; +import 'package:provider/provider.dart'; final Map roles = { "MEMBER": "Member", @@ -7,15 +11,40 @@ final Map roles = { "ADMIN": "Administrator" }; -class MemberPartCard extends StatelessWidget { +class MemberPartCard extends StatefulWidget{ final int event; - final String role; + final String cardRole, myRole; final String team; final bool small; + final bool canEdit; + + final Function(String role) onChanged; + MemberPartCard( - {Key? key, required this.event, required this.role, required this.team, required this.small}) + {Key? key, required this.event, required this.cardRole, required this.myRole, required this.team, required this.small, required this.canEdit, required this.onChanged}) : super(key: key); + @override + _MemberPartCardState createState() => _MemberPartCardState(); +} + +class _MemberPartCardState extends State { + late String cardRole, tmpRole; + late String myRole; + late bool _isEditingMode; + + List> roleNames = + roles.entries.map((entry) => DropdownMenuItem(child: Text(entry.value), value: entry.key)).toList(); + + @override + void initState() { + super.initState(); + this.cardRole= widget.cardRole; + this.tmpRole= widget.cardRole; + this.myRole = widget.myRole; + this._isEditingMode = false; + } + @override Widget build(BuildContext context) { return Container( @@ -23,7 +52,7 @@ class MemberPartCard extends StatelessWidget { margin: EdgeInsets.fromLTRB(0, 20, 0, 0), padding: EdgeInsets.fromLTRB(17, 15, 17, 15), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( @@ -40,33 +69,94 @@ class MemberPartCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(team, + Text(widget.team, textAlign: TextAlign.left, style: TextStyle( fontSize: 22, - color: Colors.black, fontWeight: FontWeight.bold)), Container( decoration: BoxDecoration( color: Colors.green, borderRadius: BorderRadius.circular(5)), child: Padding( - padding: EdgeInsets.all(small ? 4.0 : 8.0), + padding: EdgeInsets.all(widget.small ? 4.0 : 8.0), child: Text( - "SINFO " + event.toString(), - style: TextStyle(fontSize: small ? 12 : 16), + "SINFO " + widget.event.toString(), + style: TextStyle(fontSize: widget.small ? 12 : 16), ), ), ), ], ), - Divider(), - Text(roles[role]!, - textAlign: TextAlign.left, - style: TextStyle(fontSize: 18, color: Colors.black)) + Divider(color: Theme.of(context).dividerColor,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + roleWidget, + Visibility( + visible: widget.canEdit, + child: tralingButton + ) + ], + ), ], ), ]), ); } + + Widget get roleWidget { + if (_isEditingMode) { + + if(myRole=="COORDINATOR"){ + roleNames.removeWhere((option) => option.value == "ADMIN"); + } + + return DropdownButton( + items: roleNames, + value: cardRole, + hint: new Text ("Select a Role"), + onChanged: (value) => setState((){ + cardRole = value.toString(); + }), + ); + } else + return Text(roles[cardRole]!, + textAlign: TextAlign.left, + style: TextStyle(fontSize: 18)); + } + + Widget get tralingButton { + if (_isEditingMode) { + return IconButton( + icon: Icon(Icons.check), + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + iconSize: 20, + onPressed: saveChange, + ); + } else + return IconButton( + icon: Icon(Icons.edit), + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + iconSize: 20, + onPressed: _toggleMode, + ); + } + + void _toggleMode() { + setState(() { + _isEditingMode = !_isEditingMode; + }); + } + + void saveChange() { + _toggleMode(); + if(this.tmpRole!=this.cardRole){ + widget.onChanged(this.cardRole); + this.tmpRole=this.cardRole; + } + } + } diff --git a/frontend/lib/components/router.dart b/frontend/lib/components/router.dart index 43b5249a..89861f6f 100644 --- a/frontend/lib/components/router.dart +++ b/frontend/lib/components/router.dart @@ -5,11 +5,13 @@ import 'package:frontend/routes/UnknownScreen.dart'; import 'package:frontend/routes/Wrapper.dart'; import 'package:frontend/routes/company/AddCompanyForm.dart'; import 'package:frontend/routes/company/CompanyListWidget.dart'; +import 'package:frontend/routes/meeting/AddMeetingForm.dart'; import 'package:frontend/routes/member/AddMemberForm.dart'; import 'package:frontend/routes/member/MemberListWidget.dart'; import 'package:frontend/routes/speaker/SpeakerListWidget.dart'; import 'package:frontend/routes/speaker/AddSpeakerForm.dart'; import 'package:frontend/routes/teams/AddTeamMemberForm.dart'; +import 'package:frontend/routes/session/AddSessionForm.dart'; class Routes { static const String BaseRoute = '/'; @@ -22,6 +24,8 @@ class Routes { static const String ShowAllMembers = '/all/members'; static const String AddMember = '/add/member'; static const String AddTeamMember = '/add/teamMember'; + static const String AddMeeting = '/add/meeting'; + static const String AddSession = '/add/session'; } Route generateRoute(RouteSettings settings) { @@ -46,6 +50,10 @@ Route generateRoute(RouteSettings settings) { return MaterialPageRoute(builder: (context) => AddTeamMemberForm()); case Routes.AddMember: return SlideRoute(page: AddMemberForm()); + case Routes.AddMeeting: + return SlideRoute(page: AddMeetingForm()); + case Routes.AddSession: + return SlideRoute(page: AddSessionForm()); default: return MaterialPageRoute(builder: (context) => UnknownScreen()); } diff --git a/frontend/lib/components/status.dart b/frontend/lib/components/status.dart index f5a933a3..dc57a7e3 100644 --- a/frontend/lib/components/status.dart +++ b/frontend/lib/components/status.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/models/participation.dart'; diff --git a/frontend/lib/components/threadCard.dart b/frontend/lib/components/threadCard.dart index fbb4a19e..479cc97b 100644 --- a/frontend/lib/components/threadCard.dart +++ b/frontend/lib/components/threadCard.dart @@ -242,18 +242,19 @@ class ThreadCardHeader extends StatelessWidget { ), Row( children: [ - if (owner) - Padding( - padding: const EdgeInsets.all(8.0), - child: IconButton( - onPressed: () {}, icon: Icon(Icons.delete)), - ), - if (owner) - Padding( - padding: const EdgeInsets.all(8.0), - child: IconButton( - onPressed: () {}, icon: Icon(Icons.edit)), - ), + // TODO: Implement edit and delete thread + // if (owner) + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: IconButton( + // onPressed: () {}, icon: Icon(Icons.delete)), + // ), + // if (owner) + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: IconButton( + // onPressed: () {}, icon: Icon(Icons.edit)), + // ), Padding( padding: const EdgeInsets.all(8.0), child: Text( diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index bf5e4ebd..2e7f60c0 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -3,6 +3,9 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/routes/company/CompanyTableNotifier.dart'; +import 'package:frontend/routes/member/MemberNotifier.dart'; +import 'package:frontend/routes/meeting/MeetingsNotifier.dart'; +import 'package:frontend/routes/session/SessionsNotifier.dart'; import 'package:frontend/routes/speaker/speakerNotifier.dart'; import 'package:frontend/models/event.dart'; import 'package:frontend/routes/teams/TeamsNotifier.dart'; @@ -11,7 +14,6 @@ import 'package:frontend/services/eventService.dart'; import 'package:provider/provider.dart'; import 'components/router.dart' as router; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:provider/provider.dart'; Future main() async { await start(); @@ -33,6 +35,15 @@ Future main() async { ChangeNotifierProvider( create: (_) => CompanyTableNotifier(companies: []), ), + ChangeNotifierProvider( + create: (_) => MemberTableNotifier(members: []), + ), + ChangeNotifierProvider( + create: (_) => MeetingsNotifier(meetings: []), + ), + ChangeNotifierProvider( + create: (_) => SessionsNotifier(sessions: []), + ), ChangeNotifierProvider( create: (_) => AuthService(), ), diff --git a/frontend/lib/models/company.dart b/frontend/lib/models/company.dart index 35ca5c85..be285c5c 100644 --- a/frontend/lib/models/company.dart +++ b/frontend/lib/models/company.dart @@ -131,6 +131,10 @@ class Company { (element) => element.event == this.lastParticipation)!; } + String companyAsString() { + return '${this.name}'; + } + bool operator ==(o) => o is Company && id == o.id; int get hashCode => id.hashCode; } diff --git a/frontend/lib/models/meeting.dart b/frontend/lib/models/meeting.dart index 3be135f9..f732f229 100644 --- a/frontend/lib/models/meeting.dart +++ b/frontend/lib/models/meeting.dart @@ -1,18 +1,51 @@ import 'dart:convert'; +import 'package:frontend/models/thread.dart'; +import 'package:frontend/services/threadService.dart'; + class Meeting { final String id; final DateTime begin; final DateTime end; final String place; + final String title; + final String kind; + final List communicationsId; final String? minute; final MeetingParticipants participants; + List? _communications; + + Future?> get communications async { + ThreadService _threadService = ThreadService(); + + if (communicationsId.isEmpty) { + return []; + } + + if (_communications != null && _communications!.length != 0) { + return _communications; + } + + List l = []; + for (String element in communicationsId) { + Thread? t = await _threadService.getThread(element); + if (t != null) { + l.add(t); + } + } + + _communications = l; + return _communications; + } Meeting( {required this.id, required this.begin, required this.end, required this.place, + required this.title, + required this.kind, + required this.communicationsId, this.minute, required this.participants}); @@ -23,6 +56,9 @@ class Meeting { end: DateTime.parse(json['end']), place: json['place'], minute: json['minute'], + title: json['title'], + kind: json['kind'], + communicationsId: List.from(json['communications']), participants: MeetingParticipants.fromJson(json['participants'])); } @@ -32,6 +68,9 @@ class Meeting { 'end': end, 'place': place, 'minute': minute, + 'title': title, + 'kind': kind, + 'communications': communicationsId, 'participants': participants.toJson() }; @@ -49,7 +88,8 @@ class MeetingParticipants { factory MeetingParticipants.fromJson(Map json) { return MeetingParticipants( - membersIds: json['members'], companyRepIds: json['companyReps']); + membersIds: List.from(json['members']), + companyRepIds: List.from(json['companyReps'])); } Map toJson() => diff --git a/frontend/lib/models/post.dart b/frontend/lib/models/post.dart index bd81d97d..74cf77b7 100644 --- a/frontend/lib/models/post.dart +++ b/frontend/lib/models/post.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:frontend/models/member.dart'; import 'package:frontend/services/memberService.dart'; diff --git a/frontend/lib/models/session.dart b/frontend/lib/models/session.dart index 650310f0..6e02e5ae 100644 --- a/frontend/lib/models/session.dart +++ b/frontend/lib/models/session.dart @@ -1,11 +1,11 @@ import 'dart:convert'; class Session { - final String? id; + final String id; final DateTime begin; final DateTime end; final String title; - final String? description; + final String description; final String? place; final String kind; final String? companyId; @@ -14,11 +14,11 @@ class Session { final SessionTickets? tickets; Session({ - this.id, + required this.id, required this.begin, required this.end, required this.title, - this.description, + required this.description, this.place, required this.kind, this.companyId, @@ -30,23 +30,25 @@ class Session { factory Session.fromJson(Map json) { return Session( id: json['id'], - begin: DateTime(json['begin']), - end: DateTime(json['end']), + begin: DateTime.parse(json['begin']), + end: DateTime.parse(json['end']), title: json['title'], description: json['description'], place: json['place'], kind: json['kind'], companyId: json['company'], - speakersIds: json['speaker'], + speakersIds: List.from(json['speaker']), videoURL: json['videoURL'], - tickets: SessionTickets.fromJson(json['tickets']), + tickets: json['tickets'] == null + ? null + : SessionTickets.fromJson(json['tickets']), ); } Map toJson() => { 'id': id, - 'begin': begin, - 'end': end, + 'begin': begin.toIso8601String(), + 'end': end.toIso8601String(), 'title': title, 'description': description, 'place': place, @@ -139,15 +141,15 @@ class SessionTickets { factory SessionTickets.fromJson(Map json) { return SessionTickets( - start: DateTime(json['start']), - end: DateTime(json['end']), + start: DateTime.parse(json['start']), + end: DateTime.parse(json['end']), max: json['max'], ); } Map toJson() => { - 'start': start, - 'end': end, + 'start': start?.toUtc().toIso8601String(), + 'end': end?.toUtc().toIso8601String(), 'max': max, }; diff --git a/frontend/lib/models/speaker.dart b/frontend/lib/models/speaker.dart index 6fea2aff..73e43f7a 100644 --- a/frontend/lib/models/speaker.dart +++ b/frontend/lib/models/speaker.dart @@ -95,6 +95,13 @@ class Speaker { (element) => element.event == this.lastParticipation)!; } + String speakerAsString() { + return '${this.name}'; + } + + @override + String toString() => name; + bool operator ==(o) => o is Speaker && id == o.id; int get hashCode => id.hashCode; } @@ -169,4 +176,4 @@ class SpeakerLight { participations[participations.length - 1]['status']) : ParticipationStatus.NO_STATUS); } -} \ No newline at end of file +} diff --git a/frontend/lib/routes/HomeScreen.dart b/frontend/lib/routes/HomeScreen.dart index 8fa374b0..ef20ec83 100644 --- a/frontend/lib/routes/HomeScreen.dart +++ b/frontend/lib/routes/HomeScreen.dart @@ -4,14 +4,14 @@ import 'package:frontend/components/drawer.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/components/router.dart'; import 'package:frontend/main.dart'; -import 'package:frontend/models/meeting.dart'; import 'package:frontend/routes/company/CompanyTable.dart'; -import 'package:frontend/routes/meeting/MeetingCard.dart'; +import 'package:frontend/routes/meeting/MeetingPage.dart'; import 'package:frontend/routes/speaker/SpeakerTable.dart'; import 'package:frontend/routes/teams/TeamsTable.dart'; -import 'package:frontend/services/meetingService.dart'; +import 'package:frontend/services/authService.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:provider/provider.dart'; +import 'package:frontend/routes/session/SessionPage.dart'; class HomeScreen extends StatefulWidget { HomeScreen({Key? key}) : super(key: key); @@ -88,7 +88,15 @@ class _HomeScreenState extends State { Center( child: CompanyTable(), ), - Center(child: TeamTable()), + Center( + child: TeamTable(), + ), + Center( + child: MeetingPage(), + ), + Center( + child: SessionPage(), + ) ], ), ), @@ -154,6 +162,62 @@ class _HomeScreenState extends State { icon: const Icon(Icons.add), ); } + + case 4: + { + return FutureBuilder( + future: Provider.of(context).role, + builder: (context, snapshot) { + if (snapshot.hasData) { + Role r = snapshot.data as Role; + + if (r == Role.ADMIN || r == Role.COORDINATOR) { + return FloatingActionButton.extended( + onPressed: () { + Navigator.pushNamed( + context, + Routes.AddMeeting, + ); + }, + label: const Text('Create New Meeting'), + icon: const Icon(Icons.add), + ); + } else { + return Container(); + } + } else { + return Container(); + } + }); + } + + case 5: + { + return FutureBuilder( + future: Provider.of(context).role, + builder: (context, snapshot) { + if (snapshot.hasData) { + Role r = snapshot.data as Role; + + if (r == Role.ADMIN || r == Role.COORDINATOR) { + return FloatingActionButton.extended( + onPressed: () { + Navigator.pushNamed( + context, + Routes.AddSession, + ); + }, + label: const Text('Create New Session'), + icon: const Icon(Icons.add), + ); + } else { + return Container(); + } + } else { + return Container(); + } + }); + } } } } @@ -164,7 +228,7 @@ class LandingPage extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - child: MeetingList(), + child: Text("Welcome to deck2! In Progress..."), ); } } @@ -195,55 +259,23 @@ class CustomNavBar extends StatelessWidget { icon: Icon( Icons.work, )), - //FIXME: o item aqui em baixo foi colocado apenas para processo de development BottomNavigationBarItem( label: 'Teams', icon: Icon( Icons.people, )), + BottomNavigationBarItem( + label: 'Meetings', + icon: Icon( + Icons.meeting_room, + )), + BottomNavigationBarItem( + label: 'Sessions', + icon: Icon( + Icons.co_present, + )), ], onTap: onTapped, ); } } - -class MeetingList extends StatefulWidget { - const MeetingList({Key? key}) : super(key: key); - - @override - _MeetingListState createState() => _MeetingListState(); -} - -class _MeetingListState extends State - with AutomaticKeepAliveClientMixin { - final MeetingService _service = MeetingService(); - late final Future> _meetings; - - @override - void initState() { - _meetings = _service.getMeetings(); - super.initState(); - } - - @override - // TODO: implement wantKeepAlive - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return FutureBuilder( - future: _meetings, - builder: (context, snapshot) { - if (snapshot.hasData) { - List meets = snapshot.data as List; - return ListView( - children: meets.map((e) => MeetingCard(meeting: e)).toList(), - ); - } else { - return CircularProgressIndicator(); - } - }, - ); - } -} diff --git a/frontend/lib/routes/company/AddCompanyForm.dart b/frontend/lib/routes/company/AddCompanyForm.dart index 1e96afc6..30a9a737 100644 --- a/frontend/lib/routes/company/AddCompanyForm.dart +++ b/frontend/lib/routes/company/AddCompanyForm.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/models/company.dart'; diff --git a/frontend/lib/routes/company/CompanyListWidget.dart b/frontend/lib/routes/company/CompanyListWidget.dart index 693791d6..d93cabb1 100644 --- a/frontend/lib/routes/company/CompanyListWidget.dart +++ b/frontend/lib/routes/company/CompanyListWidget.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/components/ListViewCard.dart'; import 'package:frontend/components/appbar.dart'; diff --git a/frontend/lib/routes/company/CompanyScreen.dart b/frontend/lib/routes/company/CompanyScreen.dart index 8c607028..a9f53863 100644 --- a/frontend/lib/routes/company/CompanyScreen.dart +++ b/frontend/lib/routes/company/CompanyScreen.dart @@ -1,25 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:frontend/components/EditableCard.dart'; import 'package:frontend/components/addThreadForm.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/components/participationCard.dart'; -import 'package:frontend/components/router.dart'; import 'package:frontend/components/threadCard.dart'; import 'package:frontend/models/company.dart'; -import 'package:frontend/models/thread.dart'; import 'package:frontend/routes/company/CompanyTableNotifier.dart'; import 'package:frontend/routes/company/EditCompanyForm.dart'; import 'package:frontend/routes/speaker/speakerNotifier.dart'; import 'package:frontend/components/status.dart'; import 'package:frontend/main.dart'; -import 'package:frontend/models/speaker.dart'; import 'package:frontend/models/participation.dart'; -import 'package:frontend/routes/speaker/EditSpeakerForm.dart'; import 'package:frontend/services/companyService.dart'; -import 'package:frontend/services/speakerService.dart'; import 'package:provider/provider.dart'; import 'package:collection/collection.dart'; @@ -127,72 +121,73 @@ class _CompanyScreenState extends State bool small = constraints.maxWidth < App.SIZE; return Consumer(builder: (context, notif, child) { return Scaffold( - body: DefaultTabController( - length: 4, - child: Column( - children: [ - CompanyBanner( - company: widget.company, - statusChangeCallback: (step, context) { - companyChangedCallback( - context, - fs: _companyService.stepParticipationStatus( - id: widget.company.id, step: step), - ); - }, - onEdit: (context, _comp) { - companyChangedCallback(context, company: _comp); - }, - ), - TabBar( - isScrollable: small, - controller: _tabController, - tabs: [ - Tab(text: 'Details'), - Tab(text: 'BillingInfo'), - Tab(text: 'Participations'), - Tab(text: 'Communications'), - ], - ), - Expanded( - child: TabBarView(controller: _tabController, children: [ - DetailsScreen( - company: widget.company, - ), - Container( - child: Center(child: Text('Work in progress :)')), - ), - ParticipationList( - company: widget.company, - onParticipationChanged: - (Map body) async { - await companyChangedCallback( - context, - fs: _companyService.updateParticipation( - id: widget.company.id, - notes: body['notes'], - member: body['member'], - partner: body['partner'], - confirmed: body['confirmed'], - ), - ); - }, - onParticipationAdded: () => - companyChangedCallback(context, - fs: _companyService.addParticipation( - id: widget.company.id, - partner: false, - )), - ), - CommunicationsList( - participations: widget.company.participations ?? [], - small: small), - ]), - ), - ], + appBar: CustomAppBar(disableEventChange: true), + body: DefaultTabController( + length: 4, + child: Column( + children: [ + CompanyBanner( + company: widget.company, + statusChangeCallback: (step, context) { + companyChangedCallback( + context, + fs: _companyService.stepParticipationStatus( + id: widget.company.id, step: step), + ); + }, + onEdit: (context, _comp) { + companyChangedCallback(context, company: _comp); + }, + ), + TabBar( + isScrollable: small, + controller: _tabController, + tabs: [ + Tab(text: 'Details'), + Tab(text: 'BillingInfo'), + Tab(text: 'Participations'), + Tab(text: 'Communications'), + ], + ), + Expanded( + child: TabBarView(controller: _tabController, children: [ + DetailsScreen( + company: widget.company, + ), + Container( + child: Center(child: Text('Work in progress :)')), + ), + ParticipationList( + company: widget.company, + onParticipationChanged: + (Map body) async { + await companyChangedCallback( + context, + fs: _companyService.updateParticipation( + id: widget.company.id, + notes: body['notes'], + member: body['member'], + partner: body['partner'], + confirmed: body['confirmed'], + ), + ); + }, + onParticipationAdded: () => + companyChangedCallback(context, + fs: _companyService.addParticipation( + id: widget.company.id, + partner: false, + )), + ), + CommunicationsList( + participations: widget.company.participations ?? [], + small: small), + ]), + ), + ], + ), ), - ), - ); + floatingActionButton: _fabAtIndex(context)); }); }); } diff --git a/frontend/lib/routes/company/CompanyTable.dart b/frontend/lib/routes/company/CompanyTable.dart index 37f725f4..97bc44e4 100644 --- a/frontend/lib/routes/company/CompanyTable.dart +++ b/frontend/lib/routes/company/CompanyTable.dart @@ -1,8 +1,4 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/models/company.dart'; import 'package:frontend/routes/company/CompanyTableNotifier.dart'; diff --git a/frontend/lib/routes/company/CompanyTableNotifier.dart b/frontend/lib/routes/company/CompanyTableNotifier.dart index d1ad2036..15bb0288 100644 --- a/frontend/lib/routes/company/CompanyTableNotifier.dart +++ b/frontend/lib/routes/company/CompanyTableNotifier.dart @@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart'; import 'package:frontend/components/status.dart'; import 'package:frontend/models/company.dart'; import 'package:frontend/models/participation.dart'; -import 'package:frontend/models/speaker.dart'; class CompanyTableNotifier extends ChangeNotifier { List companies; diff --git a/frontend/lib/routes/company/EditCompanyForm.dart b/frontend/lib/routes/company/EditCompanyForm.dart index 7c6b3146..ed639eb6 100644 --- a/frontend/lib/routes/company/EditCompanyForm.dart +++ b/frontend/lib/routes/company/EditCompanyForm.dart @@ -3,15 +3,10 @@ import 'dart:io'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; -import 'package:frontend/components/appbar.dart'; import 'package:frontend/models/company.dart'; -import 'package:frontend/models/speaker.dart'; import 'package:frontend/services/companyService.dart'; -import 'package:frontend/services/speakerService.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:image/image.dart' as img; class EditCompanyForm extends StatefulWidget { final Company company; diff --git a/frontend/lib/routes/meeting/AddMeetingForm.dart b/frontend/lib/routes/meeting/AddMeetingForm.dart new file mode 100644 index 00000000..c6c1acde --- /dev/null +++ b/frontend/lib/routes/meeting/AddMeetingForm.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/appbar.dart'; +import 'package:frontend/models/meeting.dart'; +import 'package:frontend/routes/meeting/MeetingsNotifier.dart'; +import 'package:frontend/services/meetingService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class AddMeetingForm extends StatefulWidget { + AddMeetingForm({Key? key}) : super(key: key); + + @override + _AddMeetingFormState createState() => _AddMeetingFormState(); +} + +const kinds = ["Event", "Team", "Company"]; + +class _AddMeetingFormState extends State { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _placeController = TextEditingController(); + final _beginDateController = TextEditingController(); + final _endDateController = TextEditingController(); + final _meetingService = MeetingService(); + + DateTime? dateTime; + DateTime? _begin; + DateTime? _end; + + String _kind = ""; + + void _submit() async { + if (_formKey.currentState!.validate()) { + var title = _titleController.text; + var place = _placeController.text; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Uploading')), + ); + + Meeting? m = await _meetingService.createMeeting( + _begin!.toUtc(), _end!.toUtc(), place, _kind, title); + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.add(m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + action: SnackBarAction( + label: 'Undo', + onPressed: () { + _meetingService.deleteMeeting(m.id); + notifier.remove(m); + }, + ), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + Navigator.pop(context); + } + } + + Future _selectDateTime(BuildContext context, bool isBegin) async { + final datePicker = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2025), + ); + + final timePicker = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }); + + if (datePicker != null && timePicker != null) { + if (isBegin) { + _begin = DateTime(datePicker.year, datePicker.month, datePicker.day, + timePicker.hour, timePicker.minute); + } else { + _end = DateTime(datePicker.year, datePicker.month, datePicker.day, + timePicker.hour, timePicker.minute); + } + } + } + + String getDateTime(DateTime dateTime) { + return DateFormat('yyyy-MM-dd HH:mm').format(dateTime); + } + + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.title), + labelText: "Title *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _placeController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a place'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.place), + labelText: "Place *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _beginDateController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a beggining date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "Begin Date *", + ), + readOnly: true, //prevents editing the date in the form field + onTap: () async { + await _selectDateTime(context, true); + String formattedDate = getDateTime(_begin!); + + setState(() { + _beginDateController.text = formattedDate; + }); + }, + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _endDateController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter an ending date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "End Date *", + ), + readOnly: true, //prevents editing the date in the form field + onTap: () async { + await _selectDateTime(context, false); + String formattedDate = getDateTime(_end!); + + setState(() { + _endDateController.text = formattedDate; + }); + }, + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButtonFormField( + validator: (value) { + if (value == null) { + return 'Please enter the kind of the meeting'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.category), + labelText: "Kind *", + ), + items: kinds.map((String kind) { + return new DropdownMenuItem(value: kind, child: Text(kind)); + }).toList(), + onChanged: (newValue) { + // do other stuff with _category + setState(() => _kind = newValue.toString()); + })), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(), + child: const Text('Submit'), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + CustomAppBar appBar = CustomAppBar( + disableEventChange: true, + ); + return Scaffold( + body: Stack(children: [ + Container( + margin: EdgeInsets.fromLTRB(0, appBar.preferredSize.height, 0, 0), + child: _buildForm()), + appBar, + ]), + ); + } +} diff --git a/frontend/lib/routes/meeting/AddMeetingMemberForm.dart b/frontend/lib/routes/meeting/AddMeetingMemberForm.dart new file mode 100644 index 00000000..32517b3e --- /dev/null +++ b/frontend/lib/routes/meeting/AddMeetingMemberForm.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/SearchResultWidget.dart'; +import 'package:frontend/models/meeting.dart'; +import 'package:frontend/models/member.dart'; +import 'package:frontend/services/meetingService.dart'; +import 'package:frontend/services/memberService.dart'; + +enum MemberType { MEMBER, COMPANYREP } + +class AddMeetingMemberForm extends StatefulWidget { + final Meeting? meeting; + final void Function(BuildContext, Meeting?)? onEditMeeting; + + AddMeetingMemberForm({Key? key, this.meeting, this.onEditMeeting}) + : super(key: key); + + @override + _AddMeetingMemberForm createState() => _AddMeetingMemberForm(); +} + +class _AddMeetingMemberForm extends State { + final _formKey = GlobalKey(); + final _textController = TextEditingController(); + MeetingService service = MeetingService(); + MemberService _memberService = MemberService(); + late Future> membs; + TextEditingController _searchMembersController = TextEditingController(); + late MemberType type; + String _memberID = ''; + String _memberName = ''; + bool disappearSearchResults = false; + + String _convertMemberType(MemberType mt) { + switch (mt) { + case MemberType.MEMBER: + return "MEMBER"; + case MemberType.COMPANYREP: + return "COMPANYREP"; + default: + return ""; + } + } + + void _submit(BuildContext context) async { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Adding member...')), + ); + Meeting? m = await service.addMeetingParticipant( + id: widget.meeting!.id, + memberID: _memberID, + type: _convertMemberType(type)); + if (m != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + widget.onEditMeeting!(context, m); + + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + + Navigator.pop(context); + } + } + } + + List getResults(double height) { + if (_searchMembersController.text.length > 1 && !disappearSearchResults) { + return [ + Container( + decoration: new BoxDecoration( + color: Theme.of(context).cardColor, + ), + child: FutureBuilder( + future: this.membs, + builder: (context, snapshot) { + if (snapshot.hasData) { + List membsMatched = snapshot.data as List; + print("membs matched:" + membsMatched.toString()); + return searchResults(membsMatched, height); + } else { + return Center(child: CircularProgressIndicator()); + } + })) + ]; + } else { + return []; + } + } + + Widget searchResults(List members, double listHeight) { + List results = getListCards(members); + return Container( + constraints: BoxConstraints(maxHeight: listHeight), + child: ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: results.length, + itemBuilder: (BuildContext context, int index) { + return results[index]; + })); + } + + void _getMemberData(String id, String name) { + _memberID = id; + _memberName = name; + _searchMembersController.text = name; + disappearSearchResults = true; + setState(() {}); + } + + List getListCards(List members) { + List results = []; + if (members.length != 0) { + results.add(getDivider("Members")); + results.addAll(members.map((e) => SearchResultWidget( + member: e, + getMemberData: _getMemberData, + ))); + } + return results; + } + + Widget getDivider(String name) { + return Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + Container( + child: Text(name, style: TextStyle(fontSize: 18)), + margin: EdgeInsets.fromLTRB(0, 8, 0, 4), + ), + ], + )); + } + + @override + Widget build(BuildContext context) { + List types = ["Member", "Company Representative"]; + return Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _searchMembersController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a member'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.groups), labelText: "Member *"), + onChanged: (newQuery) { + setState(() {}); + if (_searchMembersController.text.length > 1) { + this.membs = _memberService.getMembers( + name: _searchMembersController.text); + } + })), + ...getResults(MediaQuery.of(context).size.height * 0.3), + Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButtonFormField( + icon: Icon(Icons.tag), + items: types + .map((e) => + DropdownMenuItem(value: e, child: Text(e))) + .toList(), + value: types[0], + selectedItemBuilder: (BuildContext context) { + return types.map((e) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Container(child: Text(e)), + ); + }).toList(); + }, + onChanged: (next) { + setState(() { + if (next == types[1]) { + type = MemberType.COMPANYREP; + } else if (next == types[0]) { + type = MemberType.MEMBER; + } + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(context), + child: const Text('Submit'), + ), + ), + ], + )); + } +} diff --git a/frontend/lib/routes/meeting/EditMeetingForm.dart b/frontend/lib/routes/meeting/EditMeetingForm.dart new file mode 100644 index 00000000..c524621e --- /dev/null +++ b/frontend/lib/routes/meeting/EditMeetingForm.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/models/meeting.dart'; +import 'package:frontend/routes/meeting/MeetingsNotifier.dart'; +import 'package:frontend/services/meetingService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class EditMeetingForm extends StatefulWidget { + final Meeting meeting; + EditMeetingForm({Key? key, required this.meeting}) : super(key: key); + + @override + _EditMeetingFormState createState() => _EditMeetingFormState(); +} + +const kinds = ["Event", "Team", "Company"]; + +class _EditMeetingFormState extends State { + final _formKey = GlobalKey(); + late TextEditingController _titleController; + late TextEditingController _placeController; + late TextEditingController _beginDateController; + late TextEditingController _endDateController; + final _meetingService = MeetingService(); + + late DateTime _begin; + late DateTime _end; + late String _kind; + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.meeting.title); + _placeController = TextEditingController(text: widget.meeting.place); + _beginDateController = + TextEditingController(text: getDateTime(widget.meeting.begin)); + _endDateController = + TextEditingController(text: getDateTime(widget.meeting.end)); + _begin = widget.meeting.begin; + _end = widget.meeting.end; + _kind = widget.meeting.kind; + } + + String getDateTime(DateTime dateTime) { + return DateFormat('yyyy-MM-dd HH:mm').format(dateTime); + } + + void _submit() async { + if (_formKey.currentState!.validate()) { + var title = _titleController.text; + var place = _placeController.text; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Uploading')), + ); + + Meeting? m = await _meetingService.updateMeeting( + widget.meeting.id, _begin.toUtc(), _end.toUtc(), place, _kind, title); + + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + } + + Future _selectDateTime(BuildContext context, bool isBegin) async { + final datePicker = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2025), + ); + + final timePicker = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }); + + if (datePicker != null && timePicker != null) { + if (isBegin) { + _begin = DateTime(datePicker.year, datePicker.month, datePicker.day, + timePicker.hour, timePicker.minute); + } else { + _end = DateTime(datePicker.year, datePicker.month, datePicker.day, + timePicker.hour, timePicker.minute); + } + } + } + + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.title), + labelText: "Title *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _placeController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a place'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.place), + labelText: "Place *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _beginDateController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a beggining date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "Begin Date *", + ), + readOnly: true, //prevents editing the date in the form field + onTap: () async { + await _selectDateTime(context, true); + String formattedDate = getDateTime(_begin); + + setState(() { + _beginDateController.text = formattedDate; + }); + }, + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _endDateController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter an ending date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "End Date *", + ), + readOnly: true, //prevents editing the date in the form field + onTap: () async { + await _selectDateTime(context, false); + String formattedDate = getDateTime(_end); + + setState(() { + _endDateController.text = formattedDate; + }); + }, + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButtonFormField( + validator: (value) { + if (value == null) { + return 'Please enter the kind of the meeting'; + } + return null; + }, + // Transforming TEAM or COMPANY or EVENT into Team or Company or Event + value: + "${widget.meeting.kind[0].toUpperCase()}${widget.meeting.kind.substring(1).toLowerCase()}", + decoration: const InputDecoration( + icon: const Icon(Icons.category), + labelText: "Kind *", + ), + items: kinds.map((String kind) { + return new DropdownMenuItem(value: kind, child: Text(kind)); + }).toList(), + onChanged: (newValue) { + // do other stuff with _category + setState(() => _kind = newValue.toString()); + })), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(), + child: const Text('Submit'), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return _buildForm(); + } +} diff --git a/frontend/lib/routes/meeting/MeetingCard.dart b/frontend/lib/routes/meeting/MeetingCard.dart index 8cf1df05..e19ff8b8 100644 --- a/frontend/lib/routes/meeting/MeetingCard.dart +++ b/frontend/lib/routes/meeting/MeetingCard.dart @@ -1,85 +1,358 @@ -import 'package:flutter/cupertino.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:frontend/components/blurryDialog.dart'; +import 'package:frontend/main.dart'; import 'package:frontend/models/meeting.dart'; +import 'package:frontend/routes/meeting/EditMeetingForm.dart'; +import 'package:frontend/routes/meeting/MeetingScreen.dart'; +import 'package:frontend/routes/meeting/MeetingsNotifier.dart'; +import 'package:frontend/services/authService.dart'; +import 'package:frontend/services/meetingService.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class MeetingCard extends StatelessWidget { final Meeting meeting; + final _meetingService = MeetingService(); MeetingCard({Key? key, required this.meeting}) : super(key: key); - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - print("go to meeting page"); //TODO + void _editMeetingModal(context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return FractionallySizedBox( + heightFactor: 0.7, + child: Container( + child: EditMeetingForm(meeting: meeting), + )); }, - child: Card( + ); + } + + void _deleteMeetingDialog(context, id) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog('Warning', + 'Are you sure you want to delete meeting ${meeting.title}?', () { + _deleteMeeting(context, id); + }); + }, + ); + } + + void _deleteMeeting(context, id) async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Deleting')), + ); + + Meeting? m = await _meetingService.deleteMeeting(id); + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.remove(m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + + void _uploadMeetingMinute(context) async { + if (meeting.minute!.isNotEmpty) { + Uri uri = Uri.parse(meeting.minute!); + if (!await launchUrl(uri)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Error downloading minutes')), + ); + } + } else { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf'], + ); + if (result != null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Uploading')), + ); + + PlatformFile minute = result.files.first; + + Meeting? m = await _meetingService.uploadMeetingMinute( + id: meeting.id, minute: minute); + + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + } + } + + void _deleteMeetingMinuteDialog(context) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog('Warning', + 'Are you sure you want to delete meeting minutes of ${meeting.title}?', + () { + _deleteMeetingMinute(context); + }); + }, + ); + } + + void _deleteMeetingMinute(context) async { + Meeting? m = await _meetingService.deleteMeetingMinute(meeting.id); + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + + Widget _buildMeetingCard(BuildContext context) { + double _dateCardWidth = 120.0, + _dateFontSize = 25.0, + _titleFontSize = 23.0, + _placeDateFontSize = 20.0, + _cardMargin = 25.0, + _dateMargins = 15.0, + _iconsMargin = 8.0, + _titleUpBottomMargin = 20.0, + _titleLeftMargin = 15.0; + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < App.SIZE) { + _dateCardWidth = 50.0; + _dateFontSize = 14.0; + _titleFontSize = 16.0; + _placeDateFontSize = 14.0; + } + return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.0), ), - margin: EdgeInsets.all(25.0), - child: Container( - height: 100.0, - child: Row( - children: [ - Container( - height: 100.0, - width: 100.0, - child: Column( - children: [ - Container( - margin: EdgeInsets.only(top: 20.0), - child: Text( - DateFormat.d().format(meeting.begin), - ), + margin: EdgeInsets.all(_cardMargin), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MeetingScreen(meeting: meeting)), + ); + }, + child: Stack(children: [ + Positioned.fill( + child: Container( + alignment: Alignment.centerLeft, + // This child will fill full height, replace it with your leading widget + child: Container( + width: _dateCardWidth, + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: _dateMargins), + child: Text( + DateFormat.d().format(meeting.begin), + style: TextStyle( + color: Colors.white, fontSize: _dateFontSize), + ), + ), + Container( + child: Text( + DateFormat.MMM() + .format(meeting.begin) + .toUpperCase(), + style: TextStyle( + color: Colors.white, fontSize: _dateFontSize), + ), + ), + Container( + margin: EdgeInsets.only(bottom: _dateMargins), + child: Text( + DateFormat.y().format(meeting.begin).toUpperCase(), + style: TextStyle( + color: Colors.white, fontSize: _dateFontSize), + ), + ), + ], ), - Container( - child: Text( - DateFormat.MMM().format(meeting.begin), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + topLeft: Radius.circular(5.0)), + image: DecorationImage( + image: AssetImage("assets/banner_background.png"), + fit: BoxFit.fill, ), ), - ], - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - topLeft: Radius.circular(5.0)), - image: DecorationImage( - image: AssetImage("assets/banner_background.png"), - fit: BoxFit.fill, ), ), ), - Container( - margin: EdgeInsets.only(left: 15.0, top: 20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - child: Text( - meeting.place, - style: TextStyle(color: Colors.black, fontSize: 23.0), - textAlign: TextAlign.left, + Row( + children: [ + SizedBox(width: _dateCardWidth), + Expanded( + child: Container( + margin: EdgeInsets.only( + top: _titleUpBottomMargin, + bottom: _titleUpBottomMargin, + left: _titleLeftMargin), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: Text( + meeting.title, + style: TextStyle(fontSize: _titleFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + child: Text( + meeting.place, + style: TextStyle( + color: Colors.grey, + fontSize: _placeDateFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + padding: EdgeInsets.only(top: 5.0), + child: Text( + DateFormat.Hm().format(meeting.begin.toLocal()) + + ' - ' + + DateFormat.Hm().format(meeting.end.toLocal()), + style: TextStyle( + color: Colors.grey, + fontSize: _placeDateFontSize), + textAlign: TextAlign.left, + ), + ), + ], ), ), - Container( - padding: EdgeInsets.only(top: 5.0), - child: Text( - DateFormat.jm().format(meeting.begin) + - ' - ' + - DateFormat.jm().format(meeting.end), - style: TextStyle(color: Colors.grey, fontSize: 20.0), - textAlign: TextAlign.left, - ), - ), - ], - ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(_iconsMargin), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + _editMeetingModal(context); + }, + icon: Icon(Icons.edit), + color: const Color(0xff5c7ff2)), + FutureBuilder( + future: + Provider.of(context) + .role, + builder: (context, snapshot) { + if (snapshot.hasData) { + Role r = snapshot.data as Role; + + if (r == Role.ADMIN || + r == Role.COORDINATOR) { + return IconButton( + onPressed: () => + _deleteMeetingDialog( + context, meeting.id), + icon: Icon(Icons.delete), + color: Colors.red); + } else { + return Container(); + } + } else { + return Container(); + } + }) + ]), + if (DateTime.now().isAfter(meeting.begin)) + ElevatedButton.icon( + onPressed: () => + _uploadMeetingMinute(context), + icon: Icon(Icons.article), + style: ElevatedButton.styleFrom( + primary: meeting.minute!.isNotEmpty + ? const Color(0xFF5C7FF2) + : Colors.green), + label: meeting.minute!.isNotEmpty + ? const Text("Minutes") + : const Text("Add Minutes")), + if (DateTime.now().isAfter(meeting.begin) && + meeting.minute!.isNotEmpty) + Container( + margin: const EdgeInsets.only(top: 5.0), + child: ElevatedButton.icon( + onPressed: () => + _deleteMeetingMinuteDialog(context), + icon: Icon(Icons.article), + style: ElevatedButton.styleFrom( + primary: const Color(0xFFF25C5C)), + label: const Text("Delete Minutes"))) + ])), + ), + ], ), - ], - ), - ), - ), - ); + ])), + ); + }); + } + + @override + Widget build(BuildContext context) { + return _buildMeetingCard(context); } -} \ No newline at end of file +} diff --git a/frontend/lib/routes/meeting/MeetingPage.dart b/frontend/lib/routes/meeting/MeetingPage.dart new file mode 100644 index 00000000..9f6f65f4 --- /dev/null +++ b/frontend/lib/routes/meeting/MeetingPage.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/main.dart'; +import 'package:frontend/models/meeting.dart'; +import 'package:frontend/routes/meeting/MeetingCard.dart'; +import 'package:frontend/routes/meeting/MeetingsNotifier.dart'; +import 'package:frontend/services/meetingService.dart'; +import 'package:provider/provider.dart'; + +class MeetingPage extends StatelessWidget { + const MeetingPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: MeetingList(), + ); + } +} + +class MeetingList extends StatefulWidget { + const MeetingList({Key? key}) : super(key: key); + + @override + _MeetingListState createState() => _MeetingListState(); +} + +class _MeetingListState extends State + with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { + final MeetingService _service = MeetingService(); + late final Future> _meetings; + late final TabController _tabController; + + @override + void initState() { + _meetings = _service.getMeetings(); + _tabController = TabController(length: 2, vsync: this); + super.initState(); + } + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( + future: _meetings, + builder: (context, snapshot) { + if (snapshot.hasData) { + MeetingsNotifier notifier = Provider.of(context); + + notifier.meetings = snapshot.data as List; + + return LayoutBuilder(builder: (context, constraints) { + bool small = constraints.maxWidth < App.SIZE; + return Column( + children: [ + TabBar( + isScrollable: small, + controller: _tabController, + tabs: [ + Tab(text: 'Upcoming'), + Tab(text: 'Past'), + ], + ), + Consumer( + builder: (context, cart, child) { + return Expanded( + child: TabBarView(controller: _tabController, children: [ + ListView( + children: notifier + .getUpcoming() + .map((e) => MeetingCard(meeting: e)) + .toList(), + ), + ListView( + children: notifier + .getPast() + .map((e) => MeetingCard(meeting: e)) + .toList(), + ), + ]), + ); + }, + ), + ], + ); + }); + } else { + return CircularProgressIndicator(); + } + }, + ); + } +} diff --git a/frontend/lib/routes/meeting/MeetingScreen.dart b/frontend/lib/routes/meeting/MeetingScreen.dart new file mode 100644 index 00000000..ce31d59c --- /dev/null +++ b/frontend/lib/routes/meeting/MeetingScreen.dart @@ -0,0 +1,660 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:frontend/components/ListViewCard.dart'; +import 'package:frontend/components/addThreadForm.dart'; +import 'package:frontend/components/appbar.dart'; +import 'package:frontend/components/blurryDialog.dart'; +import 'package:frontend/components/deckTheme.dart'; +import 'package:frontend/components/eventNotifier.dart'; +import 'package:frontend/components/threadCard.dart'; +import 'package:frontend/models/meeting.dart'; +import 'package:frontend/models/member.dart'; +import 'package:frontend/models/thread.dart'; +import 'package:frontend/routes/meeting/AddMeetingMemberForm.dart'; +import 'package:frontend/routes/meeting/MeetingsNotifier.dart'; +import 'package:frontend/main.dart'; +import 'package:frontend/services/meetingService.dart'; +import 'package:frontend/services/memberService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MeetingScreen extends StatefulWidget { + Meeting meeting; + + MeetingScreen({Key? key, required this.meeting}) : super(key: key); + + @override + _MeetingScreenState createState() => _MeetingScreenState(); +} + +class _MeetingScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _tabController.addListener(_handleTabIndex); + } + + @override + void dispose() { + _tabController.removeListener(_handleTabIndex); + _tabController.dispose(); + super.dispose(); + } + + void _handleTabIndex() { + setState(() {}); + } + + Future meetingChangedCallback(BuildContext context, + {Future? fm, Meeting? meeting}) async { + Meeting? m; + if (fm != null) { + m = await fm; + } else if (meeting != null) { + m = meeting; + } + if (m != null) { + Provider.of(context, listen: false).edit(m); + setState(() { + widget.meeting = m!; + }); + } + } + + void _addThreadModal(context) { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + child: AddThreadForm( + meeting: widget.meeting, + onEditMeeting: (context, _meeting) { + meetingChangedCallback(context, meeting: _meeting); + }), + ); + }, + ); + } + + void _addMember(context) { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + child: AddMeetingMemberForm( + meeting: widget.meeting, + onEditMeeting: (context, _meeting) { + meetingChangedCallback(context, meeting: _meeting); + }), + ); + }, + ); + } + + Widget? _fabAtIndex(BuildContext context) { + int latestEvent = Provider.of(context).latest.id; + int index = _tabController.index; + switch (index) { + case 0: + return FloatingActionButton.extended( + onPressed: () { + _addMember(context); + }, + label: const Text('Add New Member'), + icon: const Icon(Icons.edit), + ); + case 1: + { + return FloatingActionButton.extended( + onPressed: () { + _addThreadModal(context); + }, + label: const Text('Add Communication'), + icon: const Icon(Icons.add), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + bool small = constraints.maxWidth < App.SIZE; + return Consumer(builder: (context, notif, child) { + return Scaffold( + appBar: CustomAppBar(disableEventChange: true), + body: DefaultTabController( + length: 2, + child: Column( + children: [ + MeetingBanner( + meeting: widget.meeting, + onEdit: (context, _meeting) { + meetingChangedCallback(context, meeting: _meeting); + }, + ), + TabBar( + isScrollable: small, + controller: _tabController, + tabs: [ + Tab(text: 'Participants'), + Tab(text: 'Communications'), + ], + ), + Expanded( + child: TabBarView(controller: _tabController, children: [ + MeetingParticipants( + meeting: widget.meeting, + small: small, + onEditMeeting: (context, _meeting) { + meetingChangedCallback(context, meeting: _meeting); + }), + MeetingsCommunications( + communications: widget.meeting.communications, + small: small), + ]), + ), + ], + ), + ), + floatingActionButton: _fabAtIndex(context)); + }); + }); + } +} + +class MeetingParticipants extends StatelessWidget { + Meeting meeting; + final bool small; + final void Function(BuildContext, Meeting?)? onEditMeeting; + + MeetingParticipants( + {Key? key, + required this.meeting, + required this.small, + required this.onEditMeeting}) + : super(key: key); + + double cardWidth = 200; + MemberService _memberService = MemberService(); + MeetingService _meetingService = MeetingService(); + ScrollController _controller = ScrollController(); + + void _deleteMeetingParticipant(context, id, type, name) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog('Warning', + 'Are you sure you want to delete ${name} from meeting ${meeting.title}?', + () async { + Meeting? m = await _meetingService.deleteMeetingParticipant( + id: meeting.id, memberID: id, type: type); + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(m); + + onEditMeeting!(context, m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + }); + }, + ); + } + + Widget MeetingMembersGrid(List> _members, String type) { + if (_members != []) { + return FutureBuilder( + future: Future.wait(_members), + builder: (context, snapshot) { + if (snapshot.hasData) { + List membs = snapshot.data as List; + membs.sort((a, b) => a!.name.compareTo(b!.name)); + + cardWidth = small ? 125 : 200; + + return GridView.builder( + controller: _controller, + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + MediaQuery.of(context).size.width ~/ cardWidth, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + childAspectRatio: 0.70, + ), + itemCount: membs.length, + itemBuilder: (BuildContext context, int index) { + return Column(children: [ + ListViewCard(small: small, member: membs[index]), + ElevatedButton.icon( + onPressed: () => _deleteMeetingParticipant(context, + membs[index]!.id, type, membs[index]!.name), + icon: Icon(Icons.delete), + style: ElevatedButton.styleFrom(primary: Colors.red), + label: const Text("Delete participant")), + ]); + }); + } else { + return Center(child: CircularProgressIndicator()); + } + }); + } else { + return Text("Meeting without this kind of members"); + } + } + + Widget Separator(_text) { + return Column( + children: [ + Container( + alignment: Alignment.centerLeft, + child: Text(_text, style: TextStyle(fontSize: small ? 14 : 18)), + margin: EdgeInsets.fromLTRB(0, 8, 8, 0), + ), + Divider( + color: Colors.grey, + thickness: 1, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + List> _futureMembers = []; + if (meeting.participants.membersIds != []) { + _futureMembers = meeting.participants.membersIds! + .map((memberID) => _memberService.getMember(memberID)) + .toList(); + } + + List> _futureCompanyReps = []; + if (meeting.participants.companyRepIds != []) { + _futureCompanyReps = meeting.participants.companyRepIds! + .map((memberID) => _memberService.getMember(memberID)) + .toList(); + } + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Container( + color: Color(0xffF1F1F1), + child: Column(children: [ + Separator("Members"), + Container( + child: Center( + child: MeetingMembersGrid(_futureMembers, "MEMBER"))), + Separator("Company Reps"), + Container( + child: Center( + child: MeetingMembersGrid( + _futureCompanyReps, "COMPANYREP"))), + ])))); + } +} + +class MeetingsCommunications extends StatelessWidget { + final Future?> communications; + final bool small; + + MeetingsCommunications( + {Key? key, required this.communications, required this.small}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder( + future: communications, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text('Error'); + } + if (snapshot.connectionState == ConnectionState.done) { + List? threads = snapshot.data as List?; + if (threads == null) { + threads = []; + } + threads.sort((a, b) => b.posted.compareTo(a.posted)); + return ListView(controller: ScrollController(), children: [ + ...threads + .map( + (thread) => Padding( + padding: const EdgeInsets.all(8.0), + child: ThreadCard( + thread: thread, + small: small, + ), + ), + ) + .toList(), + ]); + } else { + return Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } +} + +class MeetingBanner extends StatelessWidget { + final Meeting meeting; + final void Function(BuildContext, Meeting?) onEdit; + final MeetingService _meetingService = MeetingService(); + + MeetingBanner({Key? key, required this.meeting, required this.onEdit}) + : super(key: key); + + void _uploadMeetingMinute(context) async { + if (meeting.minute!.isNotEmpty) { + Uri uri = Uri.parse(meeting.minute!); + if (!await launchUrl(uri)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Error downloading minutes')), + ); + } + } else { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf'], + ); + if (result != null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Uploading')), + ); + + PlatformFile minute = result.files.first; + + Meeting? m = await _meetingService.uploadMeetingMinute( + id: meeting.id, minute: minute); + + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(m); + + onEdit(context, m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + } + } + + void _deleteMeetingMinuteDialog(context) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog('Warning', + 'Are you sure you want to delete meeting minutes of ${meeting.title}?', + () { + _deleteMeetingMinute(context); + }); + }, + ); + } + + void _deleteMeetingMinute(context) async { + Meeting? m = await _meetingService.deleteMeetingMinute(meeting.id); + if (m != null) { + MeetingsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(m); + + onEdit(context, m); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + + @override + Widget build(BuildContext context) { + double _titleFontSize = 32, _infoFontSize = 20; + double lum = 0.2; + var matrix = [ + 0.2126 * lum, + 0.7152 * lum, + 0.0722 * lum, + 0, + 0, + 0.2126 * lum, + 0.7152 * lum, + 0.0722 * lum, + 0, + 0, + 0.2126 * lum, + 0.7152 * lum, + 0.0722 * lum, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]; // Greyscale matrix. Lum represents level of luminosity + return LayoutBuilder( + builder: (context, constraints) { + bool small = constraints.maxWidth < App.SIZE; + return Stack( + alignment: AlignmentDirectional.topEnd, + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + colorFilter: Provider.of(context).isDark + ? ColorFilter.matrix(matrix) + : null, + image: AssetImage('assets/banner_background.png'), + fit: BoxFit.cover, + ), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: small ? 4 : 20, vertical: small ? 5 : 25), + child: Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.all(small ? 8 : 12), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 25), + child: Text( + meeting.title.toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: _titleFontSize), + overflow: TextOverflow.ellipsis, + )), + Padding( + padding: EdgeInsets.symmetric( + horizontal: small + ? 0 + : MediaQuery.of(context).size.width / + 10), + child: Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan(children: [ + WidgetSpan( + child: Icon(Icons.calendar_today, + color: Colors.white), + ), + TextSpan( + text: ' ' + + DateFormat.d() + .format(meeting.begin) + + ' ' + + DateFormat.MMMM() + .format(meeting.begin) + .toUpperCase() + + ' ' + + DateFormat.y() + .format(meeting.begin) + .toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: _infoFontSize), + ), + ])), + RichText( + text: TextSpan(children: [ + WidgetSpan( + child: Icon(Icons.schedule, + color: Colors.white), + ), + TextSpan( + text: ' ' + + DateFormat.Hm().format( + meeting.begin.toLocal()) + + ' - ' + + DateFormat.Hm().format( + meeting.end.toLocal()), + style: TextStyle( + color: Colors.white, + fontSize: _infoFontSize), + ), + ])), + RichText( + text: TextSpan(children: [ + WidgetSpan( + child: Icon(Icons.place, + color: Colors.white), + ), + TextSpan( + text: ' ' + meeting.place, + style: TextStyle( + color: Colors.white, + fontSize: _infoFontSize), + ), + ])), + RichText( + text: TextSpan(children: [ + WidgetSpan( + child: Icon( + Icons.format_list_numbered, + color: Colors.white), + ), + TextSpan( + text: ' ' + + meeting.kind.toLowerCase(), + style: TextStyle( + color: Colors.white, + fontSize: _infoFontSize), + ), + ])), + ], + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + if (DateTime.now() + .isAfter(meeting.begin)) + ElevatedButton.icon( + onPressed: () => + _uploadMeetingMinute( + context), + icon: Icon(Icons.article), + style: ElevatedButton.styleFrom( + primary: meeting + .minute!.isNotEmpty + ? const Color( + 0xFF5C7FF2) + : Colors.green), + label: meeting + .minute!.isNotEmpty + ? const Text("Minutes") + : const Text( + "Add Minutes")), + if (DateTime.now() + .isAfter(meeting.begin) && + meeting.minute!.isNotEmpty) + Container( + margin: const EdgeInsets.only( + top: 5.0), + child: ElevatedButton.icon( + onPressed: () => + _deleteMeetingMinuteDialog( + context), + icon: Icon(Icons.article), + style: ElevatedButton + .styleFrom( + primary: const Color( + 0xFFF25C5C)), + label: const Text( + "Delete Minutes"))) + ])), + ], + )) + ], + ), + ), + ), + ], + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/lib/routes/meeting/MeetingsNotifier.dart b/frontend/lib/routes/meeting/MeetingsNotifier.dart new file mode 100644 index 00000000..e2de19a2 --- /dev/null +++ b/frontend/lib/routes/meeting/MeetingsNotifier.dart @@ -0,0 +1,34 @@ +import 'package:flutter/cupertino.dart'; +import 'package:frontend/models/meeting.dart'; + +class MeetingsNotifier extends ChangeNotifier { + List meetings; + + MeetingsNotifier({required this.meetings}); + + List getUpcoming() { + return meetings.where((m) => DateTime.now().isBefore(m.begin)).toList(); + } + + List getPast() { + return meetings.where((m) => DateTime.now().isAfter(m.begin)).toList(); + } + + void add(Meeting m) { + meetings.add(m); + notifyListeners(); + } + + void remove(Meeting m) { + meetings.removeWhere((meet) => m.id == meet.id); + notifyListeners(); + } + + void edit(Meeting m) { + int index = meetings.indexWhere((meet) => m.id == meet.id); + if (index != -1) { + meetings[index] = m; + notifyListeners(); + } + } +} diff --git a/frontend/lib/routes/member/AddMemberForm.dart b/frontend/lib/routes/member/AddMemberForm.dart index 6ac5b3ee..895d3d14 100644 --- a/frontend/lib/routes/member/AddMemberForm.dart +++ b/frontend/lib/routes/member/AddMemberForm.dart @@ -1,13 +1,7 @@ -//import 'dart:io'; -//import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -//import 'package:flutter_dropzone/flutter_dropzone.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/services/memberService.dart'; -//import 'package:image_picker/image_picker.dart'; class AddMemberForm extends StatefulWidget { AddMemberForm({Key? key}) : super(key: key); @@ -140,13 +134,13 @@ class _AddMemberFormState extends State { child: Text("CANCEL", style: TextStyle( fontSize: 14, - color: Theme.of(context).accentColor)), + color: Theme.of(context).colorScheme.secondary)), ), Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( style: ElevatedButton.styleFrom( - primary: Theme.of(context).accentColor, + primary: Theme.of(context).colorScheme.secondary, padding: EdgeInsets.symmetric(horizontal: 50), elevation: 2, shape: RoundedRectangleBorder( diff --git a/frontend/lib/routes/member/DisplayContact2.dart b/frontend/lib/routes/member/DisplayContact2.dart index 60192c39..970d68fc 100644 --- a/frontend/lib/routes/member/DisplayContact2.dart +++ b/frontend/lib/routes/member/DisplayContact2.dart @@ -1,5 +1,3 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:frontend/models/contact.dart'; import 'package:frontend/models/member.dart'; @@ -33,16 +31,21 @@ class _DisplayContactsState extends State { builder: (context, snapshot) { if (snapshot.hasData) { Role r = snapshot.data as Role; + Member me = Provider.of(context)!; - if (r == Role.ADMIN || r == Role.COORDINATOR) { + if (r == Role.ADMIN || r == Role.COORDINATOR || me.id == widget.member.id) { return FloatingActionButton.extended( - onPressed: () { - Navigator.pushReplacement( + onPressed: () async { + final bool? shouldRefresh = await Navigator.push( context, MaterialPageRoute( builder: (context) => EditContact(contact: cont, member: widget.member)), ); + if (shouldRefresh ?? false) { + this.contact = contactService.getContact(widget.member.contact!); + setState(() {}); + } }, label: const Text('Edit Contacts'), icon: const Icon(Icons.edit), diff --git a/frontend/lib/routes/member/EditContact.dart b/frontend/lib/routes/member/EditContact.dart index c7ab4557..c3daa125 100644 --- a/frontend/lib/routes/member/EditContact.dart +++ b/frontend/lib/routes/member/EditContact.dart @@ -1,16 +1,14 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/components/blurryDialog.dart'; import 'package:frontend/models/contact.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/my_flutter_app_icons.dart'; -import 'package:frontend/routes/member/MemberScreen.dart'; import 'package:frontend/services/contactService.dart'; class EditContact extends StatefulWidget { - Contact contact; - Member member; + final Contact contact; + final Member member; EditContact({Key? key, required Contact this.contact, required this.member}) : super(key: key); @@ -42,6 +40,10 @@ class _MyFormState extends State { void initState() { super.initState(); + mailsList.clear(); + mailsPersonalList.clear(); + mailsValidList.clear(); + // Copy mails for (int i = 0; i < widget.contact.mails!.length; i++) { mailsList.add(widget.contact.mails![i].mail); @@ -49,6 +51,9 @@ class _MyFormState extends State { mailsValidList.add(widget.contact.mails![i].valid); } + phonesList.clear(); + phonesValidList.clear(); + // Copy phones for (int i = 0; i < widget.contact.phones!.length; i++) { phonesList.add(widget.contact.phones![i].phone); @@ -70,174 +75,179 @@ class _MyFormState extends State { super.dispose(); } + Future showWarning(BuildContext context) async => showDialog ( + context: context, + builder: (context) => AlertDialog( + title: Text("Discard changes?"), + content: Text("Changes on this page will not be saved"), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context, false), + child: Text("Cancel") + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text("Discard") + ), + ], + ) + ); + @override Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - disableEventChange: true, - ), - body: ListView( - padding: EdgeInsets.symmetric(horizontal: 32), - physics: BouncingScrollPhysics(), - children: [ - Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Mails", - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), - TextButton( - // Add new form field - onPressed: () { - mailsList.add(''); - mailsValidList.add(false); - mailsPersonalList.add(false); - setState(() {}); - }, - child: Text('Add new')) - ], - ), - ..._getMails(), - SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Phones", - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), - TextButton( - // Add new form field - onPressed: () { - phonesList.add(''); - phonesValidList.add(false); - setState(() {}); - }, - child: Text('Add new')) - ], - ), - ..._getPhones(), - SizedBox( - height: 24, - ), - - // Socials Section - Text("Socials", - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - GetSocials(), - - SizedBox(height: 40), - - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - OutlinedButton( - style: OutlinedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - ), - onPressed: () { - //Do not remove this, otherwise it will duplicate - mailsList.clear(); - mailsValidList.clear(); - mailsPersonalList.clear(); - phonesList.clear(); - phonesValidList.clear(); - - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => - MemberScreen(member: widget.member)), - ); - }, - child: Text("CANCEL", + return WillPopScope( + onWillPop: () async { + final shouldPop = await showWarning(context); + return shouldPop ?? false; + }, + child: Scaffold( + appBar: CustomAppBar( + disableEventChange: true, + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 32), + physics: BouncingScrollPhysics(), + children: [ + Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Mails", style: TextStyle( - fontSize: 14, - color: Theme.of(context).accentColor)), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - primary: Theme.of(context).accentColor, + fontSize: 18, fontWeight: FontWeight.bold)), + TextButton( + // Add new form field + onPressed: () { + mailsList.add(''); + mailsValidList.add(false); + mailsPersonalList.add(false); + setState(() {}); + }, + child: Text('Add new')) + ], + ), + ..._getMails(), + SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Phones", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + TextButton( + // Add new form field + onPressed: () { + phonesList.add(''); + phonesValidList.add(false); + setState(() {}); + }, + child: Text('Add new')) + ], + ), + ..._getPhones(), + SizedBox( + height: 24, + ), + + // Socials Section + Text("Socials", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + GetSocials(), + + SizedBox(height: 40), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton( + style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(horizontal: 50), - elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20)), ), onPressed: () async { - if (_formKey.currentState!.validate()) { - // If the form is valid, display a snackbar. In the real world, - // you'd often call a server or save the information in a database. - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Updated Contacts')), - ); - - for (int i = 0; i < mailsList.length; i++) { - newListContactMail.add(new ContactMail( - mail: mailsList[i], - valid: mailsValidList[i], - personal: mailsPersonalList[i])); - } - - for (int i = 0; i < phonesList.length; i++) { - newListContactPhone.add(new ContactPhone( - phone: phonesList[i], - valid: phonesValidList[i])); - } - - await contactService.updateContact(new Contact( - id: widget.contact.id, - mails: newListContactMail, - phones: newListContactPhone, - socials: new ContactSocials( - facebook: facebook, - twitter: twitter, - github: github, - skype: skype, - linkedin: linkedin))); - - //Do not remove this, otherwise it will duplicate - mailsList.clear(); - mailsValidList.clear(); - mailsPersonalList.clear(); - phonesList.clear(); - phonesValidList.clear(); - - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => - MemberScreen(member: widget.member)), - ); + final shouldPop = await showWarning(context); + if (shouldPop ?? false){ + Navigator.pop(context,false); } }, - child: const Text('SUBMIT'), + child: Text("CANCEL", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.secondary)), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Theme.of(context).colorScheme.secondary, + padding: EdgeInsets.symmetric(horizontal: 50), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + ), + onPressed: () async { + if (_formKey.currentState!.validate()) { + // If the form is valid, display a snackbar. In the real world, + // you'd often call a server or save the information in a database. + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Updated Contacts')), + ); + + for (int i = 0; i < mailsList.length; i++) { + newListContactMail.add(new ContactMail( + mail: mailsList[i], + valid: mailsValidList[i], + personal: mailsPersonalList[i])); + } + + for (int i = 0; i < phonesList.length; i++) { + newListContactPhone.add(new ContactPhone( + phone: phonesList[i], + valid: phonesValidList[i])); + } + + await contactService.updateContact(new Contact( + id: widget.contact.id, + mails: newListContactMail, + phones: newListContactPhone, + socials: new ContactSocials( + facebook: facebook, + twitter: twitter, + github: github, + skype: skype, + linkedin: linkedin))); + + Navigator.of(context).pop(true); + } + }, + child: const Text('SUBMIT'), + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), - ]), + ]), + ), ); } @@ -514,8 +524,7 @@ class _MailsTextFieldsState extends State { ), ), validator: (value) { - //TODO: esta validação pode estar melhor - if (value == null || value.isEmpty || !value.contains('@')) { + if (value == null || value.isEmpty || !value.isValidEmail()) { return 'Please enter a valid mail'; } return null; @@ -582,8 +591,8 @@ class _PhonesTextFieldsState extends State { ), ), validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a phone'; + if (value == null || value.isEmpty || !value.isValidPhoneNumber()) { + return 'Please enter a valid phone number'; } return null; }, @@ -601,3 +610,18 @@ class _PhonesTextFieldsState extends State { ); } } + +extension EmailValidator on String { + bool isValidEmail() { + return RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') + .hasMatch(this); + } +} + +extension PhoneValidator on String { + bool isValidPhoneNumber() { + return RegExp(r'^[0-9]{9}$').hasMatch(this); + } +} + diff --git a/frontend/lib/routes/member/EditMemberForm.dart b/frontend/lib/routes/member/EditMemberForm.dart index 5b90a999..9cedb74d 100644 --- a/frontend/lib/routes/member/EditMemberForm.dart +++ b/frontend/lib/routes/member/EditMemberForm.dart @@ -1,18 +1,21 @@ import 'dart:io'; +import 'dart:ui'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; -import 'package:frontend/components/appbar.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/services/memberService.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:image/image.dart' as img; class EditMemberForm extends StatefulWidget { final Member member; - EditMemberForm({Key? key, required this.member}) : super(key: key); + final void Function(BuildContext, Member?) onEdit; + EditMemberForm( + {Key? key, + required this.member, + required this.onEdit}) + : super(key: key); @override _EditMemberFormState createState() => _EditMemberFormState(); @@ -33,7 +36,7 @@ class _EditMemberFormState extends State { void initState() { super.initState(); _nameController = TextEditingController(text: widget.member.name); - _istIdController = TextEditingController(text: widget.member.id); + _istIdController = TextEditingController(text: widget.member.istId); _prevImage = widget.member.image; } @@ -42,36 +45,38 @@ class _EditMemberFormState extends State { var name = _nameController.text; var istId = _istIdController.text; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Uploading')), + //FIXME try to use themes to avoid using style property + const SnackBar(content: Text('Uploading', style: TextStyle(color: Colors.white),)), ); - print('id = ${widget.member.id}'); + Member? m = await _memberService.updateMember( + id: widget.member.id, + name: name, + istid: istId); - Member? m = - await _memberService.updateMember(widget.member.id, name, istId); if (m != null && _image != null) { - //FIXME: update image - // m = kIsWeb - // ? await _memberService.updateInternalImageWeb( - // id: m.id, image: _image!) - // : await _memberService.updateInternalImage( - // id: m.id, image: File(_image!.path)); + m = kIsWeb + ? await _memberService.updateImageWeb( + id: m.id, image: _image!) + : await _memberService.updateImage( + id: m.id, image: File(_image!.path)); } if (m != null) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Done'), + content: Text('Done', style: TextStyle(color: Colors.white),), duration: Duration(seconds: 2), ), ); Navigator.pop(context); + widget.onEdit(context, m); } else { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('An error occured.')), + const SnackBar(content: Text('An error occured.', style: TextStyle(color: Colors.white),)), ); } } @@ -115,36 +120,12 @@ class _EditMemberFormState extends State { ), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - OutlinedButton( - style: OutlinedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - ), - onPressed: () => Navigator.pop(context), - child: Text("CANCEL", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).accentColor)), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - primary: Theme.of(context).accentColor, - padding: EdgeInsets.symmetric(horizontal: 50), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - ), - onPressed: () => _submit(), - child: const Text('SUBMIT'), - ), - ), - ], + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(), + child: const Text('Submit'), + ), ), ], ), @@ -177,15 +158,26 @@ class _EditMemberFormState extends State { } else { String path = _image == null ? _prevImage! : _image!.path; inkWellChild = Center( - child: kIsWeb - ? Image.network( - path, - fit: BoxFit.fill, - ) - : Image.file( - File(path), - fit: BoxFit.fill, - ), + child: + Stack( + children: [ + Center(child: kIsWeb ? + Image.network(path, fit: BoxFit.fill) + : Image.file(File(path),fit: BoxFit.fill), + ), + ClipRRect( // Clip it cleanly. + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + color: Colors.black.withOpacity(0.5), + alignment: Alignment.center, + child: Text('Change Photo', style: TextStyle(fontWeight: FontWeight.bold),), + ), + ), + ), + ] + ) + ); } @@ -263,41 +255,32 @@ class _EditMemberFormState extends State { @override Widget build(BuildContext context) { - // bool warning = _image != null && _size != null && _size! > 102400; - - return Scaffold( - appBar: CustomAppBar(disableEventChange: true,), - body: LayoutBuilder(builder: (contex, constraints) { - return Column(children: [ - _buildForm(), - ]); - })); - - // return SingleChildScrollView( - // child: LayoutBuilder( - // builder: (context, constraints) { - // if (constraints.maxWidth < 1000) { - // return Column( - // children: [_buildPicture(constraints.maxWidth / 3), _buildForm()], - // ); - // } else { - // return Column( - // children: [ - // _buildPicture(constraints.maxWidth / 6), - // warning - // ? Text( - // 'Image selected is too big!', - // style: TextStyle( - // color: Colors.red, - // ), - // ) - // : Container(), - // _buildForm() - // ], - // ); - // } - // }, - // ), - // ); + bool warning = _image != null && _size != null && _size! > 102400; + return SingleChildScrollView( + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 1000) { + return Column( + children: [_buildPicture(constraints.maxWidth / 3), _buildForm()], + ); + } else { + return Column( + children: [ + _buildPicture(constraints.maxWidth / 6), + warning + ? Text( + 'Image selected is too big!', + style: TextStyle( + color: Colors.red, + ), + ) + : Container(), + _buildForm() + ], + ); + } + }, + ), + ); } } diff --git a/frontend/lib/routes/member/InformationBox.dart b/frontend/lib/routes/member/InformationBox.dart index 54a30386..4a560f45 100644 --- a/frontend/lib/routes/member/InformationBox.dart +++ b/frontend/lib/routes/member/InformationBox.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/models/contact.dart'; import 'package:frontend/my_flutter_app_icons.dart'; @@ -23,7 +22,7 @@ class InformationBox extends StatelessWidget { margin: EdgeInsets.fromLTRB(0, 20, 0, 0), padding: EdgeInsets.fromLTRB(17, 15, 17, 15), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( @@ -41,9 +40,8 @@ class InformationBox extends StatelessWidget { textAlign: TextAlign.left, style: TextStyle( fontSize: 22, - color: Colors.black, fontWeight: FontWeight.bold)), - Divider(), + Divider(color: Theme.of(context).dividerColor,), for (int i = 0; i < contact.mails!.length; i++) showMail(mail: contact.mails![i]), ], @@ -55,7 +53,7 @@ class InformationBox extends StatelessWidget { margin: EdgeInsets.fromLTRB(0, 20, 0, 0), padding: EdgeInsets.fromLTRB(17, 15, 17, 15), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( @@ -73,9 +71,8 @@ class InformationBox extends StatelessWidget { textAlign: TextAlign.left, style: TextStyle( fontSize: 22, - color: Colors.black, fontWeight: FontWeight.bold)), - Divider(), + Divider(color: Theme.of(context).dividerColor,), for (int i = 0; i < contact.phones!.length; i++) showPhone(phone: contact.phones![i]), ], @@ -87,7 +84,7 @@ class InformationBox extends StatelessWidget { margin: EdgeInsets.fromLTRB(0, 20, 0, 0), padding: EdgeInsets.fromLTRB(17, 15, 17, 15), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( @@ -104,9 +101,8 @@ class InformationBox extends StatelessWidget { textAlign: TextAlign.left, style: TextStyle( fontSize: 22, - color: Colors.black, fontWeight: FontWeight.bold)), - Divider(), + Divider(color: Theme.of(context).dividerColor,), Row( children: [ (contact.socials!.facebook != null) @@ -184,7 +180,7 @@ class InformationBox extends StatelessWidget { SelectableText( phone.phone!, textAlign: TextAlign.left, - style: TextStyle(fontSize: 18, color: Colors.black), + style: TextStyle(fontSize: 18), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -208,7 +204,7 @@ class InformationBox extends StatelessWidget { SelectableText( mail.mail!, textAlign: TextAlign.left, - style: TextStyle(fontSize: 18, color: Colors.black), + style: TextStyle(fontSize: 18), ), Row( children: [ @@ -234,9 +230,10 @@ class InformationBox extends StatelessWidget { } } -_launchURL(String url) async { - if (await canLaunch(url)) { - await launch(url, forceWebView: true); +_launchURL(String string) async { + Uri url = Uri.parse(string); + if (await canLaunchUrl(url)) { + await launchUrl(url); } else { throw 'Could not launch $url'; } diff --git a/frontend/lib/routes/member/MemberListWidget.dart b/frontend/lib/routes/member/MemberListWidget.dart index 6120d412..5c1d69f7 100644 --- a/frontend/lib/routes/member/MemberListWidget.dart +++ b/frontend/lib/routes/member/MemberListWidget.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/components/ListViewCard.dart'; import 'package:frontend/components/appbar.dart'; diff --git a/frontend/lib/routes/member/MemberNotifier.dart b/frontend/lib/routes/member/MemberNotifier.dart new file mode 100644 index 00000000..10ac135c --- /dev/null +++ b/frontend/lib/routes/member/MemberNotifier.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:frontend/models/member.dart'; + +class MemberTableNotifier extends ChangeNotifier { + List members; + + MemberTableNotifier({required this.members}); + + void add(Member m) { + members.add(m); + notifyListeners(); + } + + void remove(Member m) { + members.remove(m); + notifyListeners(); + } + + void edit(Member m) { + int index = members.indexOf(m); + if (index != -1) { + members[index] = m; + notifyListeners(); + } + } +} diff --git a/frontend/lib/routes/member/MemberScreen.dart b/frontend/lib/routes/member/MemberScreen.dart index d41e4a66..e6e4dffc 100644 --- a/frontend/lib/routes/member/MemberScreen.dart +++ b/frontend/lib/routes/member/MemberScreen.dart @@ -6,9 +6,13 @@ import 'package:frontend/main.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/routes/member/DisplayContact2.dart'; import 'package:frontend/routes/member/EditMemberForm.dart'; +import 'package:frontend/routes/member/MemberNotifier.dart'; import 'package:frontend/services/authService.dart'; import 'package:frontend/services/memberService.dart'; +import 'package:frontend/services/teamService.dart'; import 'package:provider/provider.dart'; +import 'package:frontend/components/router.dart'; +import 'package:frontend/models/team.dart'; class MemberScreen extends StatefulWidget { Member member; @@ -23,8 +27,6 @@ class _MemberScreen extends State with SingleTickerProviderStateMixin { late final TabController _tabController; - _MemberScreen({Key? key}); - @override void initState() { super.initState(); @@ -43,6 +45,22 @@ class _MemberScreen extends State setState(() {}); } + Future memberChangedCallback(BuildContext context, + {Future? fm, Member? member}) async { + Member? m; + if (fm != null) { + m = await fm; + } else if (member != null) { + m = member; + } + if (m != null) { + context.read().edit(m); + setState(() { + widget.member = m!; + }); + } + } + @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { @@ -52,7 +70,11 @@ class _MemberScreen extends State body: DefaultTabController( length: 2, child: Column(children: [ - MemberBanner(member: widget.member), + MemberBanner( + member: widget.member, + onEdit: (context, _member) { + memberChangedCallback(context, member: _member); + }), TabBar( isScrollable: small, controller: _tabController, @@ -78,8 +100,21 @@ class _MemberScreen extends State class MemberBanner extends StatefulWidget { final Member member; + final void Function(BuildContext, Member?) onEdit; + + const MemberBanner({Key? key, required this.member, required this.onEdit}) + : super(key: key); - const MemberBanner({Key? key, required this.member}) : super(key: key); + void _editMemberModal(context) { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + child: EditMemberForm(member: member, onEdit: this.onEdit), + ); + }, + ); + } @override _MemberBannerState createState() => _MemberBannerState(); @@ -92,19 +127,17 @@ class _MemberBannerState extends State { builder: (context, snapshot) { if (snapshot.hasData) { Role r = snapshot.data as Role; + Member me = Provider.of(context)!; - if (r == Role.ADMIN || r == Role.COORDINATOR) { + if (r == Role.ADMIN || + r == Role.COORDINATOR || + me.id == widget.member.id) { return Positioned( bottom: 15, right: 15, child: GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EditMemberForm(member: widget.member)), - ); + widget._editMemberModal(context); }, child: Container( height: 40, @@ -196,6 +229,8 @@ class DisplayParticipations extends StatefulWidget { class _DisplayParticipationsState extends State { MemberService memberService = new MemberService(); + TeamService teamService = new TeamService(); + AuthService authService = new AuthService(); late Future> memberParticipations; List participations = []; @@ -207,26 +242,72 @@ class _DisplayParticipationsState extends State { } @override - Widget build(BuildContext conext) => Scaffold( + Widget build(BuildContext context) => Scaffold( body: FutureBuilder( - future: memberParticipations, - builder: (context, snapshot) { + future: Future.wait( + [memberParticipations, Provider.of(context).role]), + builder: (context, AsyncSnapshot> snapshot) { if (snapshot.hasData) { List memParticipations = - snapshot.data as List; - + snapshot.data![0] as List; + Role r = snapshot.data![1] as Role; + Member me = Provider.of(context)!; return Scaffold( backgroundColor: Color.fromRGBO(186, 196, 242, 0.1), body: ListView( padding: EdgeInsets.symmetric(horizontal: 32), - physics: BouncingScrollPhysics(), - children: memParticipations.reversed - .map((e) => MemberPartCard( - event: e.event!, - role: e.role!, - team: e.team!, - small: widget.small)) - .toList(), + children: [ + ListView.builder( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemCount: memParticipations.length, + itemBuilder: (BuildContext context, int index) { + var e = memParticipations.reversed.elementAt(index); + return MemberPartCard( + event: e.event!, + cardRole: e.role!, + myRole: r.name, + team: e.team!, + small: widget.small, + canEdit: (authService.convert(e.role!) == + Role.ADMIN) + ? (r == Role.ADMIN) + : (r == Role.ADMIN || r == Role.COORDINATOR), + onChanged: (role) async { + List teamsByName = + await teamService.getTeams(name: e.team); + Team? team = + await teamService.updateTeamMemberRole( + teamsByName[0].id!, + widget.member.id, + role); + if (team == null) + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Unable to change that role', + style: TextStyle( + color: Colors.white, + backgroundColor: Colors.red))), + ); + else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Updated member role', + style: TextStyle(color: Colors.white), + )), + ); + if (me.id == widget.member.id) { + await authService.signOut(); + Navigator.pushReplacementNamed( + context, Routes.LoginRoute); + } + } + }); + }, + ), + ], ), ); } else { diff --git a/frontend/lib/routes/participation/participationScreen.dart b/frontend/lib/routes/participation/participationScreen.dart index 0b696d90..40cc416c 100644 --- a/frontend/lib/routes/participation/participationScreen.dart +++ b/frontend/lib/routes/participation/participationScreen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/components/EditableCard.dart'; import 'package:frontend/components/appbar.dart'; diff --git a/frontend/lib/routes/session/AddSessionForm.dart b/frontend/lib/routes/session/AddSessionForm.dart new file mode 100644 index 00000000..cd40c657 --- /dev/null +++ b/frontend/lib/routes/session/AddSessionForm.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/appbar.dart'; +import 'package:frontend/models/company.dart'; +import 'package:frontend/models/session.dart'; +import 'package:frontend/models/speaker.dart'; +import 'package:frontend/routes/session/SessionsNotifier.dart'; +import 'package:frontend/services/companyService.dart'; +import 'package:frontend/services/sessionService.dart'; +import 'package:frontend/services/speakerService.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; + +class AddSessionForm extends StatefulWidget { + AddSessionForm({Key? key}) : super(key: key); + + @override + _AddSessionFormState createState() => _AddSessionFormState(); +} + +const kinds = ["Talk", "Presentation", "Workshop"]; + +class _AddSessionFormState extends State { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _placeController = TextEditingController(); + final _beginDateController = TextEditingController(); + final _endDateController = TextEditingController(); + final _descriptionController = TextEditingController(); + final _videoURLController = TextEditingController(); + final _ticketBeginDateController = TextEditingController(); + final _ticketEndDateController = TextEditingController(); + final _sessionService = SessionService(); + + late Future> speakers; + + SpeakerService speakerService = new SpeakerService(); + CompanyService companyService = new CompanyService(); + + DateTime? dateTime; + DateTime? _begin; + DateTime? _end; + DateTime? _beginTicket; + DateTime? _endTicket; + List speakersIds = []; + String? companyId; + + String _kind = ""; + bool value = false; + double _currentTicketsValue = 0; + bool _ticketsOn = false; + + void _submit() async { + if (_formKey.currentState!.validate()) { + var title = _titleController.text; + var description = _descriptionController.text; + var place = _placeController.text; + var maxTickets = _currentTicketsValue; + var videoURL = _videoURLController.text; + + var sessionTickets = new SessionTickets( + max: maxTickets as int, start: _beginTicket, end: _endTicket); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Uploading')), + ); + + Session? s = await _sessionService.createSession( + _begin!.toUtc(), + _end!.toUtc(), + place, + _kind, + title, + description, + speakersIds, + companyId, + videoURL, + sessionTickets); + + if (s != null) { + SessionsNotifier notifier = + Provider.of(context, listen: false); + notifier.add(s); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + action: SnackBarAction( + label: 'Undo', + onPressed: () { + _sessionService.deleteSession(s.id); + notifier.remove(s); + }, + ), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + Navigator.pop(context); + } + } + + Widget _buildForm() { + return Form( + key: _formKey, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column(children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.title), + labelText: "Title *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _descriptionController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a description'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.description), + labelText: "Description *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: FormBuilderDateTimePicker( + name: 'beginDate', + controller: _beginDateController, + validator: (value) { + if (value == null) { + return 'Please enter a beggining date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "Begin Date *", + ), + onChanged: (value) => {_begin = value}, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: FormBuilderDateTimePicker( + name: 'endDate', + controller: _endDateController, + validator: (value) { + if (value == null) { + return 'Please enter an ending date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "End Date *", + ), + onChanged: (value) => {_end = value}, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: FormBuilderDropdown( + name: 'kind', + validator: (value) { + if (value == null) { + return 'Please enter the kind of session'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.category), + labelText: "Kind *", + ), + items: kinds.map((String kind) { + return new DropdownMenuItem(value: kind, child: Text(kind)); + }).toList(), + onChanged: (newValue) { + setState(() => _kind = newValue.toString()); + }), + ), + Padding( + padding: (_kind == "Talk") + ? const EdgeInsets.all(8.0) + : EdgeInsets.all(0), + child: (_kind == "Talk") + ? DropdownSearch.multiSelection( + asyncItems: (String) => speakerService.getSpeakers(), + itemAsString: (Speaker u) => u.speakerAsString(), + popupProps: PopupPropsMultiSelection.menu( + showSearchBox: true, + ), + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: const InputDecoration( + icon: const Icon(Icons.star), + labelText: "Speaker *", + ), + ), + validator: (value) { + if (_kind == "Talk") { + if (value == null || value.isEmpty) { + return 'Please enter a speaker'; + } + return null; + } + return null; + }, + onChanged: (List speakers) { + speakersIds.clear(); + for (var speaker in speakers) { + speakersIds.add(speaker.id); + } + }, + clearButtonProps: ClearButtonProps(isVisible: true), + ) + : null, + ), + Padding( + padding: (_kind == "Workshop" || _kind == "Presentation") + ? const EdgeInsets.all(8.0) + : EdgeInsets.all(0), + child: (_kind == "Workshop" || _kind == "Presentation") + ? DropdownSearch( + asyncItems: (String) => companyService.getCompanies(), + itemAsString: (Company u) => u.companyAsString(), + popupProps: PopupProps.menu( + showSearchBox: true, + ), + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: const InputDecoration( + icon: const Icon(Icons.business), + labelText: "Company *", + ), + ), + validator: (value) { + if (_kind == "Workshop" || _kind == "Presentation") { + if (value == null) { + return 'Please enter a company'; + } + return null; + } + return null; + }, + onChanged: (Company? company) { + companyId = company!.id; + }, + clearButtonProps: ClearButtonProps(isVisible: true), + ) + : null, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _placeController, + decoration: const InputDecoration( + icon: const Icon(Icons.place), + labelText: "Place ", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _videoURLController, + decoration: const InputDecoration( + icon: const Icon(Icons.video_call), + labelText: "VideoURL ", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Icon( + Icons.receipt, + color: Color.fromARGB(255, 124, 123, 123), + size: 25.0, + ), + Text( + "Add tickets ", + style: TextStyle( + fontSize: 17.0, + color: Color.fromARGB(255, 102, 101, 101)), + textAlign: TextAlign.right, + ), + Switch( + onChanged: (bool value) { + setState(() { + _ticketsOn = value; + }); + }, + value: _ticketsOn, + activeColor: Color.fromARGB(255, 19, 214, 77), + activeTrackColor: Color.fromARGB(255, 97, 233, 138), + inactiveThumbColor: Color.fromARGB(255, 216, 30, 30), + inactiveTrackColor: Color.fromARGB(255, 245, 139, 139), + ), + ], + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: (_ticketsOn == true) + ? Row( + children: [ + Text( + "Maximum number of tickets *", + style: TextStyle( + fontSize: 17.0, + color: Color.fromARGB(255, 102, 101, 101)), + textAlign: TextAlign.right, + ), + Slider( + value: _currentTicketsValue, + max: 100, + divisions: 100, + label: _currentTicketsValue.round().toString(), + onChanged: (double value) { + setState(() { + _currentTicketsValue = value; + }); + }, + ), + ], + ) + : null, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: (_ticketsOn == true) + ? FormBuilderDateTimePicker( + name: 'ticketBeginDate', + controller: _ticketBeginDateController, + validator: (value) { + if (value == null) { + return 'Please enter a beggining date for ticket availability'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "Ticket availability begin date *", + ), + onChanged: (value) => {_beginTicket = value}, + ) + : null), + Padding( + padding: const EdgeInsets.all(8.0), + child: (_ticketsOn == true) + ? FormBuilderDateTimePicker( + name: 'ticketEndDate', + controller: _ticketEndDateController, + validator: (value) { + if (value == null) { + return 'Please enter an end date for ticket availability '; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "Ticket availability end date *", + ), + onChanged: (value) => {_endTicket = value}, + ) + : null), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(), + child: const Text('Submit'), + ), + ), + ]), + )); + } + + @override + Widget build(BuildContext context) { + CustomAppBar appBar = CustomAppBar( + disableEventChange: true, + ); + return Scaffold( + body: Stack(children: [ + Container( + margin: EdgeInsets.fromLTRB(0, appBar.preferredSize.height, 0, 0), + child: _buildForm()), + appBar, + ]), + ); + } +} diff --git a/frontend/lib/routes/session/EditSessionForm.dart b/frontend/lib/routes/session/EditSessionForm.dart new file mode 100644 index 00000000..e9e4f117 --- /dev/null +++ b/frontend/lib/routes/session/EditSessionForm.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/models/session.dart'; +import 'package:frontend/routes/session/SessionsNotifier.dart'; +import 'package:frontend/services/sessionService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; + +class EditSessionForm extends StatefulWidget { + final Session session; + EditSessionForm({Key? key, required this.session}) : super(key: key); + + @override + _EditSessionFormState createState() => _EditSessionFormState(); +} + +const kinds = ["Talk", "Presentation", "Workshop"]; + +class _EditSessionFormState extends State { + final _formKey = GlobalKey(); + late TextEditingController _titleController; + late TextEditingController _descriptionController; + late TextEditingController _placeController; + late TextEditingController _beginDateController; + late TextEditingController _endDateController; + late TextEditingController _videoURLController; + final _sessionService = SessionService(); + + late DateTime _begin; + late DateTime _end; + late String _kind; + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.session.title); + _descriptionController = + TextEditingController(text: widget.session.description); + _placeController = TextEditingController(text: widget.session.place); + _videoURLController = TextEditingController(text: widget.session.videoURL); + _beginDateController = + TextEditingController(text: getDateTime(widget.session.begin)); + _endDateController = + TextEditingController(text: getDateTime(widget.session.end)); + _begin = widget.session.begin; + _end = widget.session.end; + _kind = widget.session.kind; + } + + String getDateTime(DateTime dateTime) { + return DateFormat('yyyy-MM-dd HH:mm').format(dateTime); + } + + void _submit() async { + if (_formKey.currentState!.validate()) { + var title = _titleController.text; + var description = _descriptionController.text; + var place = _placeController.text; + var videoURL = _videoURLController.text; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Uploading')), + ); + + Session? s = await _sessionService.updateSession(Session( + id: widget.session.id, + begin: _begin, + end: _end, + title: title, + description: description, + place: place, + kind: widget.session.kind, + companyId: widget.session.companyId, + speakersIds: widget.session.speakersIds, + videoURL: videoURL, + tickets: widget.session.tickets)); + + if (s != null) { + SessionsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(s); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + } + + Future _selectDateTime(BuildContext context, bool isBegin) async { + final datePicker = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2025), + ); + + final timePicker = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }); + + if (datePicker != null && timePicker != null) { + if (isBegin) { + _begin = DateTime(datePicker.year, datePicker.month, datePicker.day, + timePicker.hour, timePicker.minute); + } else { + _end = DateTime(datePicker.year, datePicker.month, datePicker.day, + timePicker.hour, timePicker.minute); + } + } + } + + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.title), + labelText: "Title *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _descriptionController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a description'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.description), + labelText: "Description *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: FormBuilderDateTimePicker( + name: 'beginDate', + controller: _beginDateController, + initialValue: widget.session.begin, + validator: (value) { + if (value == null) { + return 'Please enter a beggining date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "Begin Date *", + ), + onChanged: (value) => {_begin = value!}, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: FormBuilderDateTimePicker( + name: 'endDate', + initialValue: widget.session.end, + controller: _endDateController, + validator: (value) { + if (value == null) { + return 'Please enter an ending date'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.calendar_today), + labelText: "End Date *", + ), + onChanged: (value) => {_end = value!}, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _placeController, + decoration: const InputDecoration( + icon: const Icon(Icons.place), + labelText: "Place ", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _videoURLController, + decoration: const InputDecoration( + icon: const Icon(Icons.video_call), + labelText: "VideoURL ", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(), + child: const Text('Submit'), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return _buildForm(); + } +} diff --git a/frontend/lib/routes/session/SessionCard.dart b/frontend/lib/routes/session/SessionCard.dart new file mode 100644 index 00000000..eecaea4a --- /dev/null +++ b/frontend/lib/routes/session/SessionCard.dart @@ -0,0 +1,312 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:frontend/components/blurryDialog.dart'; +import 'package:frontend/main.dart'; +import 'package:frontend/models/session.dart'; +import 'package:frontend/routes/session/EditSessionForm.dart'; +import 'package:frontend/routes/session/SessionsNotifier.dart'; +import 'package:frontend/services/authService.dart'; +import 'package:frontend/services/sessionService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SessionCard extends StatelessWidget { + final Session session; + final _sessionService = SessionService(); + + double _dateCardWidth = 120.0, + _dateFontSize = 30.0, + _titleFontSize = 23.0, + _descriptionFontSize = 15.0, + _placeDateFontSize = 20.0, + _cardMargin = 25.0, + _dateMargins = 25.0, + _iconsMargin = 8.0, + _titleUpBottomMargin = 5.0, + _titleLeftMargin = 15.0; + + SessionCard({Key? key, required this.session}) : super(key: key); + + void _editSessionModal(context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return FractionallySizedBox( + heightFactor: 0.7, + child: Container( + child: EditSessionForm(session: session), + )); + }, + ); + } + + void _deleteSessionDialog(context, id) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog('Warning', + 'Are you sure you want to delete session ${session.title}?', () { + _deleteSession(context, id); + }); + }, + ); + } + + void _deleteSession(context, id) async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Deleting')), + ); + + Session? s = await _sessionService.deleteSession(id); + if (s != null) { + SessionsNotifier notifier = + Provider.of(context, listen: false); + notifier.remove(s); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + + Widget _buildSessionCard(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < App.SIZE) { + _dateCardWidth = 50.0; + _dateFontSize = 14.0; + _titleFontSize = 16.0; + _placeDateFontSize = 14.0; + } + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + side: BorderSide(color: Colors.indigo, width: 2)), + margin: EdgeInsets.all(_cardMargin), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + children: [ + Container( + alignment: Alignment.centerLeft, + child: Container( + width: _dateCardWidth, + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: _dateMargins), + child: Text( + DateFormat.d().format(session.begin), + style: TextStyle( + color: Colors.white, fontSize: _dateFontSize), + ), + ), + Container( + margin: EdgeInsets.only(bottom: _dateMargins), + child: Text( + DateFormat.MMM() + .format(session.begin) + .toUpperCase(), + style: TextStyle( + color: Colors.white, fontSize: _dateFontSize), + ), + ), + ], + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + topLeft: Radius.circular(5.0)), + image: DecorationImage( + image: AssetImage("assets/banner_background.png"), + fit: BoxFit.fill, + ), + ), + ), + ), + buildText(context) + ], + )), + ); + }); + } + + Widget buildText(BuildContext context) { + final DateTime? _beginTicket = session.tickets?.start; + final DateTime? _endTicket = session.tickets?.end; + final String? _maxTickets = session.tickets?.max.toString(); + + return ExpansionTile( + // childrenPadding: EdgeInsets.all(16), + title: Text( + session.title, + style: TextStyle(fontSize: _titleFontSize), + textAlign: TextAlign.left, + ), + children: [ + Stack(alignment: AlignmentDirectional.topStart, children: [ + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only( + top: _titleUpBottomMargin, + bottom: _titleUpBottomMargin, + left: _titleLeftMargin), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(bottom: _titleUpBottomMargin), + child: Text( + session.kind, + style: TextStyle(fontSize: _titleFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + margin: EdgeInsets.only( + top: _titleUpBottomMargin, + bottom: _titleUpBottomMargin), + child: Text( + session.description, + style: TextStyle(fontSize: _descriptionFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + margin: EdgeInsets.only( + top: _titleUpBottomMargin, + bottom: _titleUpBottomMargin), + child: Text( + session.place ?? 'No place available yet', + style: TextStyle(fontSize: _placeDateFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + padding: EdgeInsets.only(top: 5.0), + child: Text( + DateFormat.jm().format(session.begin.toLocal()) + + ' - ' + + DateFormat.jm().format(session.end.toLocal()), + style: TextStyle(fontSize: _placeDateFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + child: Text( + session.videoURL ?? 'No video available yet', + style: TextStyle(fontSize: _placeDateFontSize), + textAlign: TextAlign.left, + ), + ), + Container( + child: (session.tickets != null) + ? Container( + padding: + EdgeInsets.only(top: 5.0, bottom: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tickets', + style: TextStyle( + fontSize: _placeDateFontSize), + textAlign: TextAlign.left, + ), + Text( + 'Available from ' + + DateFormat.yMd() + .add_jm() + .format(_beginTicket!.toLocal()) + + ' to ' + + DateFormat.yMd() + .add_jm() + .format(_endTicket!.toLocal()), + style: TextStyle(fontSize: 15.0), + textAlign: TextAlign.left, + ), + Text( + 'Quantity: ' + _maxTickets!, + style: TextStyle(fontSize: 15.0), + textAlign: TextAlign.left, + ), + ], + )) + : null, + ), + ], + ), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(_iconsMargin), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + _editSessionModal(context); + }, + icon: Icon(Icons.edit), + color: const Color(0xff5c7ff2)), + FutureBuilder( + future: + Provider.of(context).role, + builder: (context, snapshot) { + if (snapshot.hasData) { + Role r = snapshot.data as Role; + + if (r == Role.ADMIN || + r == Role.COORDINATOR) { + return IconButton( + onPressed: () => + _deleteSessionDialog( + context, session.id), + icon: Icon(Icons.delete), + color: Colors.red); + } else { + return Container(); + } + } else { + return Container(); + } + }) + ]), + ])), + ), + ], + ), + ]), + ], + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + print("go to session page"); //TODO + }, + child: _buildSessionCard(context)); + } +} diff --git a/frontend/lib/routes/session/SessionPage.dart b/frontend/lib/routes/session/SessionPage.dart new file mode 100644 index 00000000..ec5a7c72 --- /dev/null +++ b/frontend/lib/routes/session/SessionPage.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/main.dart'; +import 'package:frontend/models/session.dart'; +import 'package:frontend/routes/session/SessionCard.dart'; +import 'package:frontend/routes/session/SessionsNotifier.dart'; +import 'package:frontend/routes/session/calendar.dart'; +import 'package:frontend/services/sessionService.dart'; +import 'package:provider/provider.dart'; +import 'package:table_calendar/table_calendar.dart'; + +class SessionPage extends StatelessWidget { + const SessionPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: SessionList(), + ); + } +} + +class SessionList extends StatefulWidget { + const SessionList({Key? key}) : super(key: key); + + @override + _SessionListState createState() => _SessionListState(); +} + +class _SessionListState extends State + with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { + final SessionService _service = SessionService(); + late final Future> _sessions; + late final TabController _tabController; + + @override + void initState() { + _sessions = _service.getSessions(); + _tabController = TabController(length: 2, vsync: this); + super.initState(); + } + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( + future: _sessions, + builder: (context, snapshot) { + if (snapshot.hasData) { + CalendarFormat format = CalendarFormat.month; + DateTime selectedDay = DateTime.now(); + DateTime focusedDay = DateTime.now(); + SessionsNotifier notifier = Provider.of(context); + + notifier.sessions = snapshot.data as List; + + var upcomingSessions = notifier.getUpcoming().toList(); + + return Calendar(sessions: upcomingSessions); + } else { + return CircularProgressIndicator(); + } + }, + ); + } +} diff --git a/frontend/lib/routes/session/SessionsNotifier.dart b/frontend/lib/routes/session/SessionsNotifier.dart new file mode 100644 index 00000000..7839980a --- /dev/null +++ b/frontend/lib/routes/session/SessionsNotifier.dart @@ -0,0 +1,34 @@ +import 'package:flutter/cupertino.dart'; +import 'package:frontend/models/session.dart'; + +class SessionsNotifier extends ChangeNotifier { + List sessions; + + SessionsNotifier({required this.sessions}); + + List getUpcoming() { + return sessions.where((s) => DateTime.now().isBefore(s.begin)).toList(); + } + + List getPast() { + return sessions.where((s) => DateTime.now().isAfter(s.begin)).toList(); + } + + void add(Session s) { + sessions.add(s); + notifyListeners(); + } + + void remove(Session s) { + sessions.removeWhere((session) => s.id == session.id); + notifyListeners(); + } + + void edit(Session s) { + int index = sessions.indexWhere((session) => s.id == session.id); + if (index != -1) { + sessions[index] = s; + notifyListeners(); + } + } +} diff --git a/frontend/lib/routes/session/calendar.dart b/frontend/lib/routes/session/calendar.dart new file mode 100644 index 00000000..04676dd3 --- /dev/null +++ b/frontend/lib/routes/session/calendar.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/blurryDialog.dart'; +import 'package:frontend/routes/session/EditSessionForm.dart'; +import 'package:frontend/routes/session/SessionsNotifier.dart'; +import 'package:frontend/services/sessionService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import '../../models/session.dart'; +import '../../services/authService.dart'; + +class Calendar extends StatefulWidget { + final List sessions; + const Calendar({Key? key, required this.sessions}) : super(key: key); + + @override + _CalendarState createState() => _CalendarState(sessions: sessions); +} + +class _CalendarState extends State { + final todaysDate = DateTime.now(); + var _focusedCalendarDate = DateTime.now(); + final _initialCalendarDate = DateTime(2000); + final _lastCalendarDate = DateTime(2050); + DateTime? selectedCalendarDate; + final titleController = TextEditingController(); + final descpController = TextEditingController(); + final List sessions; + final _sessionService = SessionService(); + CalendarFormat format = CalendarFormat.month; + + late Map> calendarSessions; + + _CalendarState({required this.sessions}); + + @override + void initState() { + selectedCalendarDate = _focusedCalendarDate; + calendarSessions = {}; + fillCalendarSessions(); + super.initState(); + } + + void fillCalendarSessions() { + for (var session in sessions) { + DateTime dateForCalendar = + DateTime(session.begin.year, session.begin.month, session.begin.day); + + setState(() { + if (calendarSessions[dateForCalendar.toUtc()] != null) { + calendarSessions[dateForCalendar.toUtc()]!.add(session); + } else { + calendarSessions[dateForCalendar!.toUtc()] = [session]; + } + }); + } + } + + @override + void dispose() { + titleController.dispose(); + descpController.dispose(); + super.dispose(); + } + + List _listOfDaySessions(DateTime dateTime) { + return calendarSessions[dateTime] ?? []; + } + + void _deleteSessionDialog(context, id) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog( + 'Warning', 'Are you sure you want to delete session?', () { + _deleteSession(context, id); + }); + }, + ); + } + + void _deleteSession(context, id) async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Deleting')), + ); + + Session? s = await _sessionService.deleteSession(id); + if (s != null) { + SessionsNotifier notifier = + Provider.of(context, listen: false); + notifier.remove(s); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + + Future _editSessionModal(context, id) async { + Future sessionFuture = _sessionService.getSession(id); + Session session = await sessionFuture; + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return FractionallySizedBox( + heightFactor: 0.7, + child: Container( + child: EditSessionForm(session: session), + )); + }, + ); + } + + Widget buildTextField( + {String? hint, required TextEditingController controller}) { + return TextField( + controller: controller, + textCapitalization: TextCapitalization.words, + decoration: InputDecoration( + labelText: hint ?? '', + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.blue, width: 1.5), + borderRadius: BorderRadius.circular( + 10.0, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.blue, width: 1.5), + borderRadius: BorderRadius.circular( + 10.0, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + TableCalendar( + focusedDay: _focusedCalendarDate, + firstDay: _initialCalendarDate, + lastDay: _lastCalendarDate, + calendarFormat: format, + onFormatChanged: (CalendarFormat _format) { + setState(() { + format = _format; + }); + }, + weekendDays: const [DateTime.sunday, 6], + startingDayOfWeek: StartingDayOfWeek.monday, + daysOfWeekHeight: 40.0, + rowHeight: 60.0, + eventLoader: _listOfDaySessions, + headerStyle: const HeaderStyle( + titleTextStyle: TextStyle( + color: Color.fromARGB(255, 63, 81, 181), fontSize: 25.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10))), + formatButtonShowsNext: false, + formatButtonTextStyle: + TextStyle(color: Colors.white, fontSize: 16.0), + formatButtonDecoration: BoxDecoration( + color: Color.fromARGB(255, 63, 81, 181), + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + ), + leftChevronIcon: Icon( + Icons.chevron_left, + color: Color.fromARGB(255, 63, 81, 181), + size: 28, + ), + rightChevronIcon: Icon( + Icons.chevron_right, + color: Color.fromARGB(255, 63, 81, 181), + size: 28, + ), + ), + daysOfWeekStyle: const DaysOfWeekStyle( + weekendStyle: + TextStyle(color: Color.fromARGB(255, 63, 81, 181)), + ), + calendarStyle: const CalendarStyle( + weekendTextStyle: + TextStyle(color: Color.fromARGB(255, 63, 81, 181)), + todayDecoration: BoxDecoration( + color: Colors.blueAccent, + shape: BoxShape.circle, + ), + selectedDecoration: BoxDecoration( + color: Color.fromARGB(255, 63, 81, 181), + shape: BoxShape.circle, + ), + markerDecoration: BoxDecoration( + color: Color.fromARGB(255, 247, 172, 42), + shape: BoxShape.circle), + ), + selectedDayPredicate: (currentSelectedDate) { + return (isSameDay(selectedCalendarDate!, currentSelectedDate)); + }, + onDaySelected: (selectedDay, focusedDay) { + if (!isSameDay(selectedCalendarDate, selectedDay)) { + setState(() { + selectedCalendarDate = selectedDay; + _focusedCalendarDate = focusedDay; + }); + } + }, + ), + ..._listOfDaySessions(selectedCalendarDate!).map( + (calSessions) => ExpansionTile( + childrenPadding: const EdgeInsets.all(8.0), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + expandedAlignment: Alignment.topLeft, + leading: const Icon( + Icons.done, + color: Colors.blue, + ), + title: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(calSessions.kind.toUpperCase() + + ' - ' + + calSessions.title), + ), + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Description: ' + calSessions.description), + Text('From ' + + DateFormat.jm() + .format(calSessions.begin.toLocal()) + + ' to ' + + DateFormat.jm() + .format(calSessions.end.toLocal())), + Text(calSessions.place ?? 'No place available yet'), + Text(calSessions.videoURL ?? + 'No video available yet'), + (calSessions.tickets != null) + ? Text('Tickets\n' + + '*Quantity: ' + + calSessions.tickets!.max.toString() + + '\n*Available from ' + + DateFormat.yMd().format( + calSessions.tickets!.start!.toLocal()) + + ' at ' + + DateFormat.jm().format( + calSessions.tickets!.start!.toLocal()) + + ' to ' + + DateFormat.yMd().format( + calSessions.tickets!.end!.toLocal()) + + calSessions.tickets!.start!.month + .toString() + + ' at ' + + DateFormat.jm().format( + calSessions.tickets!.start!.toLocal())) + : Text('No tickets available for this session'), + ], + ), + Padding( + padding: const EdgeInsets.all(40.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + (calSessions.kind == 'TALK') + ? Text('Speakers: ' + + calSessions.speakersIds.toString()) + : Text('Company: ' + + calSessions.companyId.toString()) + ]), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + _editSessionModal( + context, calSessions.id); + }, + icon: Icon(Icons.edit), + color: const Color(0xff5c7ff2)), + FutureBuilder( + future: Provider.of( + context) + .role, + builder: (context, snapshot) { + if (snapshot.hasData) { + Role r = + snapshot.data as Role; + + if (r == Role.ADMIN || + r == Role.COORDINATOR) { + return IconButton( + onPressed: () => + _deleteSessionDialog( + context, + calSessions.id), + icon: + Icon(Icons.delete), + color: Colors.red); + } else { + return Container(); + } + } else { + return Container(); + } + }) + ]), + ])), + ), + ], + ) + ]), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/lib/routes/speaker/AddSpeakerForm.dart b/frontend/lib/routes/speaker/AddSpeakerForm.dart index 43650663..cd8aa06b 100644 --- a/frontend/lib/routes/speaker/AddSpeakerForm.dart +++ b/frontend/lib/routes/speaker/AddSpeakerForm.dart @@ -3,12 +3,9 @@ import 'dart:io'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; import 'package:frontend/components/appbar.dart'; -import 'package:frontend/models/company.dart'; import 'package:frontend/models/speaker.dart'; -import 'package:frontend/services/companyService.dart'; import 'package:frontend/services/speakerService.dart'; import 'package:image_picker/image_picker.dart'; diff --git a/frontend/lib/routes/speaker/EditSpeakerForm.dart b/frontend/lib/routes/speaker/EditSpeakerForm.dart index 01c824b7..48317969 100644 --- a/frontend/lib/routes/speaker/EditSpeakerForm.dart +++ b/frontend/lib/routes/speaker/EditSpeakerForm.dart @@ -3,15 +3,11 @@ import 'dart:io'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; import 'package:frontend/components/appbar.dart'; -import 'package:frontend/models/company.dart'; import 'package:frontend/models/speaker.dart'; -import 'package:frontend/services/companyService.dart'; import 'package:frontend/services/speakerService.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:image/image.dart' as img; class EditSpeakerForm extends StatefulWidget { final Speaker speaker; diff --git a/frontend/lib/routes/speaker/SpeakerListWidget.dart b/frontend/lib/routes/speaker/SpeakerListWidget.dart index 235fe538..9325c3e4 100644 --- a/frontend/lib/routes/speaker/SpeakerListWidget.dart +++ b/frontend/lib/routes/speaker/SpeakerListWidget.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frontend/components/ListViewCard.dart'; import 'package:frontend/components/appbar.dart'; diff --git a/frontend/lib/routes/speaker/SpeakerTable.dart b/frontend/lib/routes/speaker/SpeakerTable.dart index 0cfe95fc..8f1b6d2e 100644 --- a/frontend/lib/routes/speaker/SpeakerTable.dart +++ b/frontend/lib/routes/speaker/SpeakerTable.dart @@ -1,11 +1,6 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/routes/speaker/speakerNotifier.dart'; -import 'package:frontend/components/status.dart'; import 'package:frontend/main.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/models/speaker.dart'; diff --git a/frontend/lib/routes/teams/TeamsTable.dart b/frontend/lib/routes/teams/TeamsTable.dart index 67f18bb7..cad522f1 100644 --- a/frontend/lib/routes/teams/TeamsTable.dart +++ b/frontend/lib/routes/teams/TeamsTable.dart @@ -223,8 +223,7 @@ class _TeamTableState extends State class TeamMemberRow extends StatelessWidget { final Team team; - - MemberService _memberService = MemberService(); + final MemberService _memberService = MemberService(); TeamMemberRow({Key? key, required this.team}) : super(key: key); static Widget fake() { diff --git a/frontend/lib/services/companyService.dart b/frontend/lib/services/companyService.dart index d58a66ba..430adef4 100644 --- a/frontend/lib/services/companyService.dart +++ b/frontend/lib/services/companyService.dart @@ -11,7 +11,6 @@ import 'package:frontend/models/company.dart'; import 'package:frontend/models/contact.dart'; import 'package:frontend/models/item.dart'; import 'package:frontend/models/participation.dart'; -import 'package:frontend/models/thread.dart'; import 'package:frontend/services/service.dart'; import 'package:image_picker/image_picker.dart'; @@ -329,9 +328,10 @@ class CompanyService extends Service { Response response = await dio.get("/companies/" + id + "/participation/status/next"); try { - final responseJson = json.decode(response.data!) as List; + final responseJson = json.decode(response.data!)['steps'] as List; List participationSteps = responseJson.map((e) => ParticipationStep.fromJson(e)).toList(); + return participationSteps; } on SocketException { throw DeckException('No Internet connection'); diff --git a/frontend/lib/services/contactService.dart b/frontend/lib/services/contactService.dart index 53445ed9..ebb591cd 100644 --- a/frontend/lib/services/contactService.dart +++ b/frontend/lib/services/contactService.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:frontend/components/deckException.dart'; import 'package:frontend/models/contact.dart'; -import 'package:frontend/models/member.dart'; import 'package:frontend/services/service.dart'; import 'package:dio/dio.dart'; diff --git a/frontend/lib/services/eventService.dart b/frontend/lib/services/eventService.dart index 075773f8..485407bb 100644 --- a/frontend/lib/services/eventService.dart +++ b/frontend/lib/services/eventService.dart @@ -280,7 +280,7 @@ class EventService extends Service { 'place': s.place, }; - if (s.kind == 'TALK') { + if (s.kind == 'Talk') { body['speaker'] = json.encode(s.speakersIds); } else { body['company'] = s.companyId; diff --git a/frontend/lib/services/meetingService.dart b/frontend/lib/services/meetingService.dart index fde7182c..6cb9de02 100644 --- a/frontend/lib/services/meetingService.dart +++ b/frontend/lib/services/meetingService.dart @@ -1,10 +1,16 @@ import 'dart:convert'; +import 'dart:typed_data'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:frontend/models/meeting.dart'; import 'package:frontend/services/service.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:dio/dio.dart'; import 'package:frontend/components/deckException.dart'; import 'dart:io'; +import 'package:image_picker/image_picker.dart'; + class MeetingService extends Service { Future> getMeetings( {String? team, String? company, int? event}) async { @@ -28,12 +34,13 @@ class MeetingService extends Service { } Future createMeeting(DateTime begin, DateTime end, String place, - MeetingParticipants participants) async { + String kind, String title) async { var body = { - "begin": begin, - "end": end, + "begin": begin.toIso8601String(), + "end": end.toIso8601String(), "place": place, - "participants": participants + "title": title, + "kind": kind.toUpperCase() }; Response response = await dio.post("/meetings", data: body); @@ -76,18 +83,14 @@ class MeetingService extends Service { } } - Future updateMeeting( - String id, - DateTime begin, - DateTime end, - String place, - //, MeetingParticipants participants - ) async { + Future updateMeeting(String id, DateTime begin, DateTime end, + String place, String kind, String title) async { var body = { - "begin": begin, - "end": end, - "place": place - //, "participants" : participants + "begin": begin.toIso8601String(), + "end": end.toIso8601String(), + "place": place, + "title": title, + "kind": kind.toUpperCase() }; Response response = await dio.put("/meetings/" + id, data: body); @@ -102,7 +105,106 @@ class MeetingService extends Service { } } - // Future uploadMeetingMinute(String id, String minute) async { - // // TODO: Implement : https://pub.dev/packages/dio - // } + Future addThread({ + required String id, + required String kind, + required String text, + }) async { + var body = {"kind": kind, "text": text}; + + Response response = + await dio.post("/meetings/" + id + "/thread", data: body); + try { + return Meeting.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + + Future uploadMeetingMinute( + {required String id, required PlatformFile minute}) async { + FormData formData; + if (kIsWeb) { + Uint8List file = minute.bytes!; + formData = FormData.fromMap( + { + 'minute': MultipartFile.fromBytes( + file, + filename: minute.name, + contentType: MediaType('multipart', 'form-data'), + ) + }, + ); + } else { + formData = FormData.fromMap( + {'minute': await MultipartFile.fromFile(minute.path!)}); + } + + Response response = + await dio.post('/meetings/' + id + '/minute', data: formData); + try { + return Meeting.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + + Future deleteMeetingMinute(String id) async { + Response response = await dio.delete('/meetings/' + id + '/minute'); + try { + return Meeting.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + + Future addMeetingParticipant( + {required String id, + required String memberID, + required String type}) async { + var body = {"type": type, "memberID": memberID}; + + Response response = + await dio.post('/meetings/' + id + '/participants', data: body); + try { + return Meeting.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + + Future deleteMeetingParticipant( + {required String id, + required String memberID, + required String type}) async { + var body = {"type": type, "memberID": memberID}; + + Response response = + await dio.delete('/meetings/' + id + '/participants', data: body); + try { + return Meeting.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } } diff --git a/frontend/lib/services/memberService.dart b/frontend/lib/services/memberService.dart index 521bbeec..0ab1709f 100644 --- a/frontend/lib/services/memberService.dart +++ b/frontend/lib/services/memberService.dart @@ -1,9 +1,12 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:frontend/components/deckException.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/services/service.dart'; import 'package:dio/dio.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:http_parser/http_parser.dart'; class MemberService extends Service { Future> getMembers({String? name, int? event}) async { @@ -62,15 +65,69 @@ class MemberService extends Service { } } - Future updateMember(String id, String istid, String name) async { + Future updateMember( + {required String id, + String? istid, + String? name}) async { var body = { "istid": istid, "name": name, }; - Response response = await dio.put("/members/" + id, data: body); + try { + Response response = await dio.put("/members/" + id, data: body); + + return Member.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + + Future updateImageWeb( + {required String id, required XFile image}) async { + Uint8List file = await image.readAsBytes(); + FormData formData = FormData.fromMap( + { + 'image': MultipartFile.fromBytes( + file, + filename: image.path, + contentType: MediaType('multipart', 'form-data'), + ) + }, + ); + try { + Response response = await dio.post( + '/members/' + id + '/image', + data: formData, + ); + + return Member.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } catch (e) { + if (e is DioError) { + print(e.response); + } + } + } + + Future updateImage( + {required String id, required File image}) async { + FormData formData = + FormData.fromMap({'image': await MultipartFile.fromFile(image.path)}); try { + Response response = + await dio.post('/members/' + id + '/image', data: formData); + return Member.fromJson(json.decode(response.data!)); } on SocketException { throw DeckException('No Internet connection'); diff --git a/frontend/lib/services/sessionService.dart b/frontend/lib/services/sessionService.dart index ce743f2d..b192a98c 100644 --- a/frontend/lib/services/sessionService.dart +++ b/frontend/lib/services/sessionService.dart @@ -6,6 +6,8 @@ import 'package:frontend/models/session.dart'; import 'package:frontend/services/service.dart'; import 'package:frontend/components/deckException.dart'; +import '../models/event.dart'; + class SessionService extends Service { Future> getSessions( {int? event, String? company, String? kind}) async { @@ -16,6 +18,7 @@ class SessionService extends Service { try { final responseJson = json.decode(response.data!) as List; + List sessions = responseJson.map((e) => Session.fromJson(e)).toList(); return sessions; @@ -42,6 +45,78 @@ class SessionService extends Service { } } + Future createSession( + DateTime begin, + DateTime end, + String place, + String kind, + String title, + String description, + List? speakersIds, + String? company, + String? videoURL, + SessionTickets sessionTickets) async { + var body = kind == "Talk" + ? { + "begin": begin.toIso8601String(), + "end": end.toIso8601String(), + "place": place, + "title": title, + "kind": kind.toUpperCase(), + "description": description, + "speaker": speakersIds, + "videoURL": videoURL, + // "tickets": sessionTickets.toJson(), + "company": "" + } + : { + "begin": begin.toIso8601String(), + "end": end.toIso8601String(), + "place": place, + "title": title, + "kind": kind.toUpperCase(), + "description": description, + "speaker": [], + "company": company, + "videoURL": videoURL, + // "tickets": sessionTickets.max == 0 ? null : sessionTickets.toJson() + }; + + if (sessionTickets.max != 0) { + body["tickets"] = sessionTickets.toJson(); + } + + Response response = await dio.post("/events/sessions", data: body); + + try { + int eventId = Event.fromJson(json.decode(response.data!)).id; + Future> _futureSessions = getSessions(event: eventId); + List sessions = await _futureSessions; + Session s = sessions.last; + + return s; + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + + Future deleteSession(String id) async { + Response response = await dio.delete("/sessions/" + id); + try { + return Session.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } + Future updateSession(Session session) async { var body = session.toJson(); diff --git a/frontend/lib/services/speakerService.dart b/frontend/lib/services/speakerService.dart index bd03b216..e3b5b1a6 100644 --- a/frontend/lib/services/speakerService.dart +++ b/frontend/lib/services/speakerService.dart @@ -8,7 +8,6 @@ import 'package:frontend/models/meeting.dart'; import 'package:frontend/models/participation.dart'; import 'dart:convert'; import 'package:frontend/models/speaker.dart'; -import 'package:frontend/models/thread.dart'; import 'package:frontend/services/service.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http_parser/http_parser.dart'; diff --git a/frontend/lib/services/threadService.dart b/frontend/lib/services/threadService.dart index e226ecba..8b15457b 100644 --- a/frontend/lib/services/threadService.dart +++ b/frontend/lib/services/threadService.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:frontend/components/deckException.dart'; -import 'package:frontend/models/member.dart'; import 'package:frontend/models/thread.dart'; import 'package:frontend/services/service.dart'; import 'package:dio/dio.dart'; diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 5493a2d7..3af70dc2 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: flutter_speed_dial: google_sign_in: ^5.0.7 http: ^0.13.3 + file_picker: ^5.2.0+1 flutter_dotenv: ^5.0.0 shared_preferences: ^2.0.6 responsive_scaffold: ^1.3.0+2 @@ -46,6 +47,11 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 url_launcher: ^6.0.9 + # The following adds form fields and builders + flutter_form_builder: ^7.7.0 + dropdown_search: ^5.0.3 + form_builder_extra_fields: ^8.3.0 + table_calendar: ^3.0.8 dev_dependencies: flutter_test: