diff --git a/backend/src/mongodb/member.go b/backend/src/mongodb/member.go index 02f04143..ef7fcd27 100644 --- a/backend/src/mongodb/member.go +++ b/backend/src/mongodb/member.go @@ -183,48 +183,62 @@ func (m *MembersType) GetMembers(options GetMemberOptions) ([]*models.Member, er var members []*models.Member = make([]*models.Member, 0) - query := mongo.Pipeline{ - - // filter by name first - {{ - Key: "$match", Value: bson.M{ - "name": bson.M{ - "$regex": fmt.Sprintf(".*%s.*", nameFilter), - "$options": "i", + var query mongo.Pipeline + if len(nameFilter) > 0 { + query = mongo.Pipeline{ + {{ + "$match", bson.M{ + "name": bson.M{ + "$regex": fmt.Sprintf(".*%s.*", nameFilter), + "$options": "i", + }, }, - }, - }}, - - // get all the teams on which each member is participating, - // and add them to each member correspondingly - {{ - Key: "$lookup", Value: bson.D{ - {Key: "from", Value: Teams.Collection.Name()}, - {Key: "localField", Value: "_id"}, - {Key: "foreignField", Value: "members.member"}, - {Key: "as", Value: "team"}, - }, - }}, - - // get an instance of each member for every team he/she belonged to - {{ - Key: "$unwind", Value: "$team", - }}, - - // get the event associated with each team on each member - {{ - Key: "$lookup", Value: bson.D{ - {Key: "from", Value: Events.Collection.Name()}, - {Key: "localField", Value: "team._id"}, - {Key: "foreignField", Value: "teams"}, - {Key: "as", Value: "event"}, - }, - }}, + }}, + } + } else { + query = mongo.Pipeline{ + + // filter by name first + {{ + "$match", bson.M{ + "name": bson.M{ + "$regex": fmt.Sprintf(".*%s.*", nameFilter), + "$options": "i", + }, + }, + }}, + + // get all the teams on which each member is participating, + // and add them to each member correspondingly + {{ + "$lookup", bson.D{ + {"from", Teams.Collection.Name()}, + {"localField", "_id"}, + {"foreignField", "members.member"}, + {"as", "team"}, + }, + }}, + + // get an instance of each member for every team he/she belonged to + {{ + "$unwind", "$team", + }}, + + // get the event associated with each team on each member + {{ + "$lookup", bson.D{ + {"from", Events.Collection.Name()}, + {"localField", "team._id"}, + {"foreignField", "teams"}, + {"as", "event"}, + }, + }}, - // get an instance of each member for every event he/she belonged to - {{ - Key: "$unwind", Value: "$event", - }}, + // get an instance of each member for every event he/she belonged to + {{ + "$unwind", "$event", + }}, + } } if options.Event != nil { @@ -330,6 +344,7 @@ func (m *MembersType) GetMembersParticipations(id primitive.ObjectID) ([]*models options := GetEventsOptions{} events, err := Events.GetEvents(options) if err != nil { + print("ERROR #1", err) return nil, err } @@ -339,6 +354,9 @@ func (m *MembersType) GetMembersParticipations(id primitive.ObjectID) ([]*models for _, teamID := range event.Teams { team, err := Teams.GetTeam(teamID) if err != nil { + print("TEAM ID: ", teamID.Hex()) + print("EVENT: ", event) + print("ERROR #2", err) return nil, err } for _, teamMember := range team.Members { 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/team.go b/backend/src/mongodb/team.go index f3a4d226..faeff39d 100644 --- a/backend/src/mongodb/team.go +++ b/backend/src/mongodb/team.go @@ -42,7 +42,7 @@ type UpdateTeamMemberData struct { // CreateTeamMemberData contains data needed to create a team member type CreateTeamMemberData struct { - Member primitive.ObjectID `json:"id"` + Member primitive.ObjectID `json:"member"` Role models.TeamRole `json:"role"` } 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/appbar.dart b/frontend/lib/components/appbar.dart index 325558cc..e8d5230c 100644 --- a/frontend/lib/components/appbar.dart +++ b/frontend/lib/components/appbar.dart @@ -2,6 +2,8 @@ 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'; +import 'package:frontend/main.dart'; import 'package:frontend/models/event.dart'; import 'package:frontend/routes/company/CompanyScreen.dart'; import 'package:frontend/routes/member/MemberScreen.dart'; diff --git a/frontend/lib/components/filterBarTeam.dart b/frontend/lib/components/filterBarTeam.dart index a628c3f9..43daca2e 100644 --- a/frontend/lib/components/filterBarTeam.dart +++ b/frontend/lib/components/filterBarTeam.dart @@ -1,29 +1,34 @@ import 'package:flutter/material.dart'; class FilterBarTeam extends StatefulWidget { + String currentFilter; final Function onSelected; + final List teamFilters; - FilterBarTeam({Key? key, required this.onSelected}) : super(key: key); + FilterBarTeam( + {Key? key, + required this.currentFilter, + required this.teamFilters, + required this.onSelected}) + : super(key: key); @override - FilterBarTeamState createState() => FilterBarTeamState(onSelected: onSelected); + FilterBarTeamState createState() => FilterBarTeamState( + currentFilter: currentFilter, + teamFilters: teamFilters, + onSelected: onSelected); } class FilterBarTeamState extends State { + String currentFilter; + final List teamFilters; final Function onSelected; - FilterBarTeamState({Key? key, required this.onSelected}); - - int _currentIndex = 0; - List _filters = [ - "All", - "Coordination", - "DevTeam", - "Logistics", - "Multimedia", - "Partnerships", - "Social Network", - ]; + FilterBarTeamState( + {Key? key, + required this.currentFilter, + required this.teamFilters, + required this.onSelected}); @override Widget build(BuildContext context) { @@ -35,8 +40,8 @@ class FilterBarTeamState extends State { rowChips() { List filters = []; - for (int i = 0; i < _filters.length; i++) { - filters.add(createChip(_filters[i], i)); + for (int i = 0; i < teamFilters.length; i++) { + filters.add(createChip(teamFilters[i], i)); } return Row(children: filters); } @@ -45,25 +50,26 @@ class FilterBarTeamState extends State { return Container( margin: EdgeInsets.all(7.0), child: ChoiceChip( - selected: _currentIndex == index, + selected: label.toLowerCase() == currentFilter.toLowerCase(), backgroundColor: Colors.indigo[100], shape: RoundedRectangleBorder( side: BorderSide(color: Colors.black12, width: 1), borderRadius: BorderRadius.circular(15), ), - elevation: 2, - pressElevation: 1, + elevation: 1, + pressElevation: 3, shadowColor: Colors.teal, selectedColor: Colors.indigo[400], onSelected: (bool selected) { setState(() { - _currentIndex = selected ? index : _currentIndex; - onSelected(_filters[_currentIndex].toUpperCase()); + onSelected(label.toUpperCase()); }); }, label: Text(label), labelStyle: TextStyle( - color: _currentIndex != index ? Colors.indigo[400] : Colors.white, + color: label.toLowerCase() != currentFilter.toLowerCase() + ? Colors.indigo[400] + : Colors.white, ), padding: EdgeInsets.all(6.0), ), diff --git a/frontend/lib/components/filterbar.dart b/frontend/lib/components/filterbar.dart index 402388e3..1abfe232 100644 --- a/frontend/lib/components/filterbar.dart +++ b/frontend/lib/components/filterbar.dart @@ -17,16 +17,6 @@ class FilterBarState extends State { FilterBarState({Key? key, required this.onSelected}); int _currentIndex = 0; - List _filters = [ - "All", - "Suggested", - "Contacted", - "Rejected", - "Give Up", - "Announced", - "In Conversations", - "In Negotiations" - ]; @override Widget build(BuildContext context) { diff --git a/frontend/lib/components/router.dart b/frontend/lib/components/router.dart index 96a7f294..0228e625 100644 --- a/frontend/lib/components/router.dart +++ b/frontend/lib/components/router.dart @@ -11,6 +11,8 @@ import 'package:frontend/routes/member/MemberListWidget.dart'; import 'package:frontend/routes/speaker/AddFlightInfoForm.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,8 +24,10 @@ class Routes { static const String AddSpeaker = '/add/speaker'; 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 AddFlightInfo = '/add/flightinfo'; + static const String AddSession = '/add/session'; } Route generateRoute(RouteSettings settings) { @@ -44,12 +48,16 @@ Route generateRoute(RouteSettings settings) { return SlideRoute(page: AddSpeakerForm()); case Routes.ShowAllMembers: return MaterialPageRoute(builder: (context) => MemberListWidget()); + case Routes.AddTeamMember: + return MaterialPageRoute(builder: (context) => AddTeamMemberForm()); case Routes.AddMember: return SlideRoute(page: AddMemberForm()); case Routes.AddMeeting: return SlideRoute(page: AddMeetingForm()); case Routes.AddFlightInfo: return SlideRoute(page: AddFlightInfoForm()); + case Routes.AddSession: + return SlideRoute(page: AddSessionForm()); default: return MaterialPageRoute(builder: (context) => UnknownScreen()); } diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 93a73e8b..2e7f60c0 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -5,8 +5,10 @@ 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'; import 'package:frontend/services/authService.dart'; import 'package:frontend/services/eventService.dart'; import 'package:provider/provider.dart'; @@ -39,12 +41,18 @@ Future main() async { ChangeNotifierProvider( create: (_) => MeetingsNotifier(meetings: []), ), + ChangeNotifierProvider( + create: (_) => SessionsNotifier(sessions: []), + ), ChangeNotifierProvider( create: (_) => AuthService(), ), ChangeNotifierProvider( create: (_) => BottomNavigationBarProvider(), ), + ChangeNotifierProvider( + create: (_) => TeamsNotifier(teams: []), + ), ], child: App(), )); 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/member.dart b/frontend/lib/models/member.dart index b297e15f..521d1119 100644 --- a/frontend/lib/models/member.dart +++ b/frontend/lib/models/member.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:frontend/models/contact.dart'; +import 'package:frontend/models/team.dart'; class Member { final String id; 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/models/team.dart b/frontend/lib/models/team.dart index d401b1e4..90fb9fff 100644 --- a/frontend/lib/models/team.dart +++ b/frontend/lib/models/team.dart @@ -57,7 +57,7 @@ class TeamPublic { class Team { final String? id; - final String? name; + String? name; final List? members; final List? meetings; @@ -71,7 +71,7 @@ class Team { id: json['id'], name: json['name'], members: members.map((e) => TeamMember.fromJson(e)).toList(), - meetings: meetings.length == 0? [] : meetings as List, + meetings: meetings.length == 0 ? [] : meetings as List, ); } diff --git a/frontend/lib/routes/HomeScreen.dart b/frontend/lib/routes/HomeScreen.dart index 58109c0b..ef20ec83 100644 --- a/frontend/lib/routes/HomeScreen.dart +++ b/frontend/lib/routes/HomeScreen.dart @@ -11,6 +11,7 @@ import 'package:frontend/routes/teams/TeamsTable.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); @@ -93,6 +94,9 @@ class _HomeScreenState extends State { Center( child: MeetingPage(), ), + Center( + child: SessionPage(), + ) ], ), ), @@ -186,6 +190,34 @@ class _HomeScreenState extends State { } }); } + + 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(); + } + }); + } } } } @@ -237,6 +269,11 @@ class CustomNavBar extends StatelessWidget { icon: Icon( Icons.meeting_room, )), + BottomNavigationBarItem( + label: 'Sessions', + icon: Icon( + Icons.co_present, + )), ], onTap: onTapped, ); diff --git a/frontend/lib/routes/company/CompanyScreen.dart b/frontend/lib/routes/company/CompanyScreen.dart index d331dcbc..a9f53863 100644 --- a/frontend/lib/routes/company/CompanyScreen.dart +++ b/frontend/lib/routes/company/CompanyScreen.dart @@ -121,74 +121,73 @@ class _CompanyScreenState extends State bool small = constraints.maxWidth < App.SIZE; return Consumer(builder: (context, notif, child) { return Scaffold( - 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), - ]), - ), - ], + 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) - ); + floatingActionButton: _fabAtIndex(context)); }); }); } diff --git a/frontend/lib/routes/member/MemberScreen.dart b/frontend/lib/routes/member/MemberScreen.dart index c2cd84ef..e6e4dffc 100644 --- a/frontend/lib/routes/member/MemberScreen.dart +++ b/frontend/lib/routes/member/MemberScreen.dart @@ -71,11 +71,10 @@ class _MemberScreen extends State length: 2, child: Column(children: [ MemberBanner( - member: widget.member, - onEdit: (context, _member) { + member: widget.member, + onEdit: (context, _member) { memberChangedCallback(context, member: _member); - } - ), + }), TabBar( isScrollable: small, controller: _tabController, @@ -103,11 +102,8 @@ 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, required this.onEdit}) + : super(key: key); void _editMemberModal(context) { showModalBottomSheet( @@ -133,7 +129,9 @@ class _MemberBannerState extends State { Role r = snapshot.data as Role; Member me = Provider.of(context)!; - if (r == Role.ADMIN || r == Role.COORDINATOR || me.id == widget.member.id) { + if (r == Role.ADMIN || + r == Role.COORDINATOR || + me.id == widget.member.id) { return Positioned( bottom: 15, right: 15, @@ -221,7 +219,8 @@ class _MemberBannerState extends State { class DisplayParticipations extends StatefulWidget { final Member member; final bool small; - const DisplayParticipations({Key? key, required this.member, required this.small}) + const DisplayParticipations( + {Key? key, required this.member, required this.small}) : super(key: key); @override @@ -238,63 +237,77 @@ class _DisplayParticipationsState extends State { @override void initState() { super.initState(); - this.memberParticipations = memberService.getMemberParticipations(widget.member.id); + this.memberParticipations = + memberService.getMemberParticipations(widget.member.id); } @override Widget build(BuildContext context) => Scaffold( body: FutureBuilder( - future: Future.wait([memberParticipations, Provider.of(context).role]), + future: Future.wait( + [memberParticipations, Provider.of(context).role]), builder: (context, AsyncSnapshot> snapshot) { if (snapshot.hasData) { - List memParticipations = snapshot.data![0] as List; + List memParticipations = + 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), - 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, widget.member, 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); + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 32), + 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/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/teams/AddTeamMemberForm.dart b/frontend/lib/routes/teams/AddTeamMemberForm.dart new file mode 100644 index 00000000..35c5e0f3 --- /dev/null +++ b/frontend/lib/routes/teams/AddTeamMemberForm.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/appbar.dart'; +import 'package:frontend/components/SearchResultWidget.dart'; +import 'package:frontend/models/member.dart'; +import 'package:frontend/models/team.dart'; +import 'package:frontend/services/teamService.dart'; +import 'package:frontend/services/memberService.dart'; + +final Map roles = { + "MEMBER": "Member", + "TEAMLEADER": "Team Leader" +}; + +class AddTeamMemberForm extends StatefulWidget { + final Team? team; + final void Function(BuildContext, Team?)? onEditTeam; + + AddTeamMemberForm({Key? key, this.team, this.onEditTeam}) : super(key: key); + + @override + _AddTeamMemberFormState createState() => _AddTeamMemberFormState(); +} + +class _AddTeamMemberFormState extends State { + final _formKey = GlobalKey(); + MemberService _memberService = new MemberService(); + final _searchMembersController = TextEditingController(); + TeamService service = TeamService(); + late Future> membs; + String memberRole = ""; + String _memberID = ''; + bool disappearSearchResults = false; + String role = ""; + + void _submit(BuildContext context) async { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Adding member...')), + ); + Team? t = await service.addTeamMember( + id: widget.team!.id, memberId: _memberID, role: role); + if (t != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + Navigator.pop(context); + widget.onEditTeam!(context, t); + } 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: _searchMembersController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a member'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.person_add), + labelText: "Member *", + ), + onChanged: (newQuery) { + setState(() {}); + if (_searchMembersController.text.length > 1) { + this.membs = _memberService.getMembers( + name: _searchMembersController.text); + } + })), + ...getResults(MediaQuery.of(context).size.height * 0.7), + Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButtonFormField( + validator: (value) { + if (value == null) { + return 'Please select a role'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.grid_3x3), + labelText: "Role *", + ), + items: roles.keys.map((e) { + return new DropdownMenuItem( + value: e, child: Text(roles[e]!)); + }).toList(), + onChanged: (newValue) { + setState(() => role = newValue.toString()); + }), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + padding: EdgeInsets.symmetric(horizontal: 50), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + ), + onPressed: () => _submit(context), + child: const Text('SUBMIT'), + ), + ), + ], + ), + ), + ); + } + + 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) { + widget.team!.members!.map((memberteam) { + if (id == memberteam.memberID) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Member already exist"), + content: Text("Can't add again"), + actions: [ + TextButton( + child: Text("OK"), + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + return; + } + }).toList(); + + _memberID = id; + _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) { + return _buildForm(); + } +} diff --git a/frontend/lib/routes/teams/TeamScreen.dart b/frontend/lib/routes/teams/TeamScreen.dart index fff677e6..fd1047da 100644 --- a/frontend/lib/routes/teams/TeamScreen.dart +++ b/frontend/lib/routes/teams/TeamScreen.dart @@ -2,17 +2,33 @@ import 'package:flutter/material.dart'; import 'package:frontend/models/meeting.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/models/team.dart'; +import 'package:frontend/routes/UnknownScreen.dart'; import 'package:frontend/routes/meeting/MeetingCard.dart'; import 'package:frontend/routes/member/MemberScreen.dart'; +import 'package:frontend/routes/teams/AddTeamMemberForm.dart'; +import 'package:frontend/routes/teams/TeamsNotifier.dart'; import 'package:frontend/services/meetingService.dart'; +import 'package:frontend/services/memberService.dart'; import 'package:frontend/services/teamService.dart'; +import 'package:frontend/services/authService.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; +import 'package:provider/provider.dart'; + +import '../../components/blurryDialog.dart'; + +final Map roles = { + "MEMBER": "Member", + "TEAMLEADER": "Team Leader", + "COORDINATOR": "Coordinator", + "ADMIN": "Administrator" +}; + +bool membersPage = true; class TeamScreen extends StatefulWidget { - final Team team; - final List members; + Team team; - TeamScreen({Key? key, required this.team, required this.members}) - : super(key: key); + TeamScreen({Key? key, required this.team}) : super(key: key); @override _TeamScreen createState() => _TeamScreen(); @@ -21,15 +37,16 @@ class TeamScreen extends StatefulWidget { class _TeamScreen extends State with SingleTickerProviderStateMixin { late final TabController _tabController; - TeamService teamService = new TeamService(); - - _TeamScreen({Key? key}); + TeamService _teamService = new TeamService(); + MemberService _memberService = new MemberService(); + late List> _members; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _tabController.addListener(_handleTabIndex); + _getTeamMembers(widget.team); } @override @@ -43,44 +60,438 @@ class _TeamScreen extends State setState(() {}); } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: GestureDetector( - child: Image.asset( - 'assets/logo-branco2.png', - height: 100, - width: 100, - )), + void _getTeamMembers(Team t) { + _members = t.members! + .map((teamMember) => _memberService.getMember(teamMember.memberID!)) + .toList(); + } + + Future teamChangedCallback(BuildContext context, + {Future? fm, Team? team}) async { + Team? m; + if (fm != null) { + m = await fm; + } else if (team != null) { + m = team; + } + if (m != null) { + Provider.of(context, listen: false).edit(m); + setState(() { + widget.team = m!; + _getTeamMembers(m); + }); + } + } + + void _addTeamMember(context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return FractionallySizedBox( + heightFactor: 0.7, + child: Container( + child: AddTeamMemberForm( + team: widget.team, + onEditTeam: (context, _team) { + teamChangedCallback(context, team: _team); + }), + ), + ); + }, + ); + } + + buildSpeedDial(BuildContext context) { + 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) { + if (membersPage) { + return SpeedDial( + animatedIcon: AnimatedIcons.menu_close, + animatedIconTheme: IconThemeData(size: 28.0), + backgroundColor: Color(0xff5C7FF2), + visible: true, + curve: Curves.bounceInOut, + children: [ + SpeedDialChild( + child: Icon(Icons.person_remove, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => showRemoveMemberDialog(context), + label: 'Remove Members', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + SpeedDialChild( + child: Icon(Icons.person_add, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => _addTeamMember(context), + label: 'Add Member', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + SpeedDialChild( + child: Icon(Icons.delete, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => + showDeleteTeamDialog(context, widget.team.id), + label: 'Delete Team', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + SpeedDialChild( + child: Icon(Icons.edit, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => showEditTeamDialog(), + label: 'Edit Team', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + ], + ); + } else { + return SpeedDial( + animatedIcon: AnimatedIcons.menu_close, + animatedIconTheme: IconThemeData(size: 28.0), + backgroundColor: Color(0xff5C7FF2), + visible: true, + curve: Curves.bounceInOut, + children: [ + SpeedDialChild( + child: Icon(Icons.groups, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => {}, + label: 'Add Meetings', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + SpeedDialChild( + child: Icon(Icons.delete, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => + showDeleteTeamDialog(context, widget.team.id), + label: 'Delete Team', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + SpeedDialChild( + child: Icon(Icons.edit, color: Colors.white), + backgroundColor: Colors.indigo, + onTap: () => showEditTeamDialog(), + label: 'Edit Team', + labelStyle: TextStyle( + fontWeight: FontWeight.w500, color: Colors.white), + labelBackgroundColor: Colors.black, + ), + ], + ); + } + } else { + return Container(); //CONFIRMAR + } + } else { + return Container(); + } + }); + } + + showEditTeamDialog() { + String name = widget.team.name ?? ""; + return showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Edit Team"), + content: TextFormField( + initialValue: name, + onChanged: (value) { + name = value; + }, + decoration: const InputDecoration(hintText: "New name for the team"), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, "Cancel"), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => editTeam(widget.team.id, name), + child: const Text("Update"), + ), + ], ), - body: Container( - child: DefaultTabController( - length: 2, - child: Column(children: [ - TeamBanner(team: widget.team), - TabBar( - controller: _tabController, - labelColor: Colors.black, - tabs: [ - Tab( - text: 'Members', - ), - Tab(text: 'Meetings'), - ], - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - DisplayMembers(members: widget.members), - DisplayMeeting(meetingsIds: widget.team.meetings), - ], - )) - ])), + ); + } + + showDeleteTeamDialog(context, id) { + final String name = widget.team.name ?? "team"; + return showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog( + 'Warning', 'Are you sure you want to delete meeting $name?', () { + deleteTeam(context, id); + }); + } + /* => + AlertDialog( + title: const Text("Remove Team"), + content: Text("Are you sure you want to delete \"$name\"?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, "No"), + child: const Text("No"), + ), + TextButton( + onPressed: () => deleteTeam(context, widget.team.id), + child: const Text("Yes"), + ), + ], + ),*/ + ); + } + + showRemoveMemberDialog(context) async { + String memberId = ""; + List _membs = await Future.wait(_members); + return showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Remove Team Member"), + content: DropdownButtonFormField( + validator: (value) { + if (value == null) { + return 'Please select one member'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.grid_3x3), + labelText: "MemberId *", + ), + items: _membs.map((Member? member) { + return new DropdownMenuItem( + value: member!.id, child: Text(member.name)); + }).toList(), + onChanged: (newValue) { + setState(() => memberId = newValue.toString()); + }), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, "Cancel"), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => + removeTeamMember(context, widget.team.id, memberId), + child: const Text("Delete"), + ), + ], ), ); } + + showError() { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('An error has occured. Please contact the admins'), + duration: Duration(seconds: 4), + ), + ); + return Center( + child: Icon( + Icons.error, + size: 200, + )); + } + + void removeTeamMember(context, String? id, String memberId) async { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog( + 'Warning', 'Are you sure you want to delete this member?', + () async { + Team? team = await _teamService.deleteTeamMember(id!, memberId); + if (team != null) { + TeamsNotifier notifier = + Provider.of(context, listen: false); + notifier.edit(team); + + teamChangedCallback(context, team: team); + + 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.')), + ); + } + }); + }, + ); + } + + void editTeam(String? id, String name) async { + Team? t = await _teamService.updateTeam(id!, name); + if (t != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + + teamChangedCallback(context, team: t); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + + Navigator.pop(context); + } + Navigator.pop(context, "Update"); + } + + void deleteTeam(context, String? id) async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Deleting')), + ); + Team? team = await _teamService.deleteTeam(id!); + if (team != null) { + TeamsNotifier notifier = + Provider.of(context, listen: false); + notifier.remove(team); + + 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.')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, notif, child) { + return Scaffold( + appBar: AppBar( + title: GestureDetector( + child: Image.asset( + 'assets/logo-branco2.png', + height: 100, + width: 100, + )), + ), + body: Container( + child: DefaultTabController( + length: 2, + child: Column(children: [ + TeamBanner(team: widget.team), + TabBar( + controller: _tabController, + labelColor: Colors.black, + tabs: [ + Tab( + text: 'Members', + ), + Tab(text: 'Meetings'), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + DisplayMembers(members: _members), + DisplayMeeting(meetingsIds: widget.team.meetings), + ], + )) + ])), + ), + // TODO should only appear in Members tab? + floatingActionButton: buildSpeedDial(context), + ); + }); + } +} + +class SearchResultWidget extends StatelessWidget { + final Member? member; + const SearchResultWidget({Key? key, this.member}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return UnknownScreen(); + })); + }, + child: Center( + child: ListTile( + leading: CircleAvatar( + foregroundImage: NetworkImage(getImageURL()), + backgroundImage: AssetImage( + 'assets/noImage.png', + ), + ), + title: Text(getName()), + ), + )); + } + + String getImageURL() { + if (this.member != null) { + return this.member!.image!; + } else { + //ERROR case + return ""; + } + } + + String getName() { + if (this.member != null) { + return this.member!.name; + } else { + //ERROR case + return ""; + } + } } class DisplayMeeting extends StatefulWidget { @@ -105,66 +516,68 @@ class _DisplayMeetingState extends State { @override Widget build(BuildContext context) { - List> _futureMeetings = widget.meetingsIds! - .map((m) => _meetingService.getMeeting(m)) - .toList(); + List> _futureMeetings = + widget.meetingsIds!.map((m) => _meetingService.getMeeting(m)).toList(); + + membersPage = false; return Scaffold( backgroundColor: Color.fromRGBO(186, 196, 242, 0.1), - body: (widget.meetingsIds == null) ? Container() : FutureBuilder( - future: Future.wait(_futureMeetings), - builder: (context, snapshot){ - if (snapshot.hasData) { - List meetings = snapshot.data as List; - - return ListView( - padding: EdgeInsets.symmetric(horizontal: 32), - physics: BouncingScrollPhysics(), - scrollDirection: Axis.vertical, - children: - meetings.map((e) => MeetingCard(meeting: e!)).toList()); - } else { - return Container( - child: Center( - child: Container( - height: 50, - width: 50, - child: CircularProgressIndicator(), - ), - ), - ); - } - }), - floatingActionButton: FloatingActionButton.extended( - onPressed: () {}, - label: const Text('Add Meetings'), - icon: const Icon(Icons.edit), - backgroundColor: Color(0xff5C7FF2), - ), + body: (widget.meetingsIds == null) + ? Container() + : FutureBuilder( + future: Future.wait(_futureMeetings), + builder: (context, snapshot) { + if (snapshot.hasData) { + List meetings = snapshot.data as List; + + return ListView( + padding: EdgeInsets.symmetric(horizontal: 32), + physics: BouncingScrollPhysics(), + scrollDirection: Axis.vertical, + children: meetings + .map((e) => MeetingCard(meeting: e!)) + .toList()); + } else { + return Container( + child: Center( + child: Container( + height: 50, + width: 50, + child: CircularProgressIndicator(), + ), + ), + ); + } + }), ); } } class DisplayMembers extends StatelessWidget { - final List members; + final List> members; const DisplayMembers({Key? key, required this.members}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Color.fromRGBO(186, 196, 242, 0.1), - body: ListView( - padding: EdgeInsets.symmetric(horizontal: 32), - physics: BouncingScrollPhysics(), - scrollDirection: Axis.vertical, - children: members.map((e) => ShowMember(member: e!)).toList()), - floatingActionButton: FloatingActionButton.extended( - onPressed: () {}, - label: const Text('Edit Members'), - icon: const Icon(Icons.edit), - backgroundColor: Color(0xff5C7FF2), - ), - ); + backgroundColor: Color.fromRGBO(186, 196, 242, 0.1), + body: FutureBuilder>( + future: Future.wait(members), + builder: (context, snapshot) { + if (snapshot.hasData) { + List membs = snapshot.data as List; + + return ListView( + padding: EdgeInsets.symmetric(horizontal: 32), + physics: BouncingScrollPhysics(), + scrollDirection: Axis.vertical, + children: + membs.map((e) => ShowMember(member: e!)).toList()); + } else { + return CircularProgressIndicator(); + } + })); } } @@ -179,7 +592,7 @@ class ShowMember extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => MemberScreen(member: member))); //TODO + builder: (context) => MemberScreen(member: member))); }, child: Card( color: Colors.white, diff --git a/frontend/lib/routes/teams/TeamsNotifier.dart b/frontend/lib/routes/teams/TeamsNotifier.dart new file mode 100644 index 00000000..08154f73 --- /dev/null +++ b/frontend/lib/routes/teams/TeamsNotifier.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:frontend/models/team.dart'; + +class TeamsNotifier extends ChangeNotifier { + List teams; + + TeamsNotifier({required this.teams}); + + void add(Team s) { + teams.add(s); + notifyListeners(); + } + + void remove(Team t) { + teams.removeWhere((team) => t.id == team.id); + notifyListeners(); + } + + void edit(Team t) { + int index = teams.indexWhere((team) => t.id == team.id); + if (index != -1) { + teams[index] = t; + notifyListeners(); + } + } +} diff --git a/frontend/lib/routes/teams/TeamsTable.dart b/frontend/lib/routes/teams/TeamsTable.dart index 1127107c..cad522f1 100644 --- a/frontend/lib/routes/teams/TeamsTable.dart +++ b/frontend/lib/routes/teams/TeamsTable.dart @@ -1,15 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/main.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/components/ListViewCard.dart'; import 'package:frontend/models/team.dart'; import 'package:frontend/routes/teams/TeamScreen.dart'; +import 'package:frontend/routes/teams/TeamsNotifier.dart'; import 'package:frontend/services/memberService.dart'; import 'package:frontend/services/teamService.dart'; +import 'package:frontend/components/filterBarTeam.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; +const ALL = "All"; + class TeamTable extends StatefulWidget { TeamTable({Key? key}) : super(key: key); @@ -20,88 +25,199 @@ class TeamTable extends StatefulWidget { class _TeamTableState extends State with AutomaticKeepAliveClientMixin { final TeamService _teamService = TeamService(); - late Future> teams; + String filter = ""; + late List teams; + + late Future> members; bool get wantKeepAlive => true; @override void initState() { super.initState(); + filter = ALL; } @override Widget build(BuildContext context) { super.build(context); + return Consumer(builder: (context, notif, child) { + return Scaffold( + body: FutureBuilder( + future: Future.wait([ + _teamService.getTeams( + event: Provider.of(context).event.id) + ]), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return showError(); + } else if (snapshot.hasData) { + TeamsNotifier notifier = Provider.of(context); + + List> dataTeam = + snapshot.data as List>; + + teams = dataTeam[0] as List; + + notifier.teams = teams; + + teams.sort((a, b) => a.name!.compareTo(b.name!)); + + return showTeams(); + } else { + return Center(child: CircularProgressIndicator()); + } + } else { + return Shimmer.fromColors( + baseColor: Colors.grey[400]!, + highlightColor: Colors.white, + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) => TeamMemberRow.fake(), + addAutomaticKeepAlives: true, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + ), + ); + } + }, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.startFloat, + floatingActionButton: FloatingActionButton.extended( + onPressed: showCreateTeamDialog, + label: const Text('Create New Team'), + icon: const Icon(Icons.person_add), + backgroundColor: Provider.of(context).isDark + ? Colors.grey[500] + : Colors.indigo), + ); + }); + } + + onSelected(String value) { + setState(() { + filter = value; + }); + } + + buildTeamsList() { + List filteredTeams; + if (filter.toLowerCase() == ALL.toLowerCase()) + filteredTeams = teams; + else + filteredTeams = teams + .where((team) => team.name?.toLowerCase() == filter.toLowerCase()) + .toList(); + return ListView.builder( + itemCount: filteredTeams.length, + itemBuilder: (context, index) => + TeamMemberRow(team: filteredTeams[index]), + addAutomaticKeepAlives: true, + physics: const AlwaysScrollableScrollPhysics()); + } + + showError() { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('An error has occured. Please contact the admins'), + duration: Duration(seconds: 4), + ), + ); + return Center( + child: Icon( + Icons.error, + size: 200, + )); + } + + showTeams() { return NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(8.0, 0, 0, 0), - ), + padding: const EdgeInsets.fromLTRB(8.0, 0, 0, 0), + child: FilterBarTeam( + currentFilter: filter, + teamFilters: getTeamsFilter(), + onSelected: (value) => onSelected(value), + )), ), ], - body: FutureBuilder( - future: Future.wait([ - _teamService.getTeams( - event: Provider.of(context).event.id) - ]), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('An error has occured. Please contact the admins'), - duration: Duration(seconds: 4), - ), - ); - return Center( - child: Icon( - Icons.error, - size: 200, - )); - } + body: RefreshIndicator( + onRefresh: () { + return Future.delayed(Duration.zero, () { + setState(() {}); + }); + }, + child: buildTeamsList(), + ), + ); + } - List> data = snapshot.data as List>; + showCreateTeamDialog() { + String name = ""; + return showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Create New Team"), + content: TextField( + onChanged: (value) { + name = value; + }, + decoration: const InputDecoration( + hintText: "Insert the name of the new team"), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, "Cancel"), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => createTeam(name), + child: const Text("Create"), + ), + ], + ), + ); + } - List tms = data[0] as List; + void createTeam(String name) async { + Team? t = await _teamService.createTeam(name); + if (t != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); - tms.sort((a, b) => a.name!.compareTo(b.name!)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); - return RefreshIndicator( - onRefresh: () { - return Future.delayed(Duration.zero, () { - setState(() {}); - }); - }, - child: ListView.builder( - itemCount: tms.length, - itemBuilder: (context, index) => - TeamMemberRow(team: tms[index]), - addAutomaticKeepAlives: true, - physics: const AlwaysScrollableScrollPhysics(), - ), - ); - } else { - return Shimmer.fromColors( - baseColor: Colors.grey[400]!, - highlightColor: Colors.white, - child: ListView.builder( - itemCount: 5, - itemBuilder: (context, index) => TeamMemberRow.fake(), - addAutomaticKeepAlives: true, - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics()), - ), - ); - } - }, - ), - ); + TeamsNotifier notifier = + Provider.of(context, listen: false); + notifier.add(t); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + + Navigator.pop(context); + } + Navigator.pop(context, "Create"); + } + + getTeamsFilter() { + List filters = + teams.map((team) => team.name).whereType().toList(); + filters.insert(0, ALL); + return filters; } } @@ -187,8 +303,8 @@ class TeamMemberRow extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => TeamScreen( - team: team, members: membs))); + builder: (context) => + TeamScreen(team: team))); }, child: Column( children: [ diff --git a/frontend/lib/services/companyService.dart b/frontend/lib/services/companyService.dart index 9f809a5a..430adef4 100644 --- a/frontend/lib/services/companyService.dart +++ b/frontend/lib/services/companyService.dart @@ -328,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/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/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/teamService.dart b/frontend/lib/services/teamService.dart index 804d5107..e2dc29c0 100644 --- a/frontend/lib/services/teamService.dart +++ b/frontend/lib/services/teamService.dart @@ -97,9 +97,12 @@ class TeamService extends Service { } } - Future addTeamMember(String id, String member, String role) async { + Future addTeamMember( + {required String? id, + required String memberId, + required String role}) async { var body = { - "member": member, + "member": memberId, "role": role, }; @@ -118,14 +121,14 @@ class TeamService extends Service { } Future updateTeamMemberRole( - String id, String memberID, Member member, String role) async { + String id, String memberId, String role) async { var body = { - "member": member, + "member": memberId, "role": role, }; Response response = - await dio.put(baseURL + '/$id' + '/members' + '/$memberID', data: body); + await dio.put(baseURL + '/$id' + '/members' + '/$memberId', data: body); try { return Team.fromJson(json.decode(response.data!)); diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 5360db89..3af70dc2 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -23,6 +23,7 @@ environment: dependencies: flutter: sdk: flutter + flutter_speed_dial: google_sign_in: ^5.0.7 http: ^0.13.3 file_picker: ^5.2.0+1 @@ -41,11 +42,16 @@ dependencies: collection: ^1.15.0 image: ^3.0.5 shimmer: ^2.0.0 - timeago: ^3.1.0 + timeago: ^3.1.0 # The following adds the Cupertino Icons font to your application. # 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: