From ac93a4bd71945886e3a1bd9ac6f1932d85bae6c7 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 28 Mar 2021 00:01:01 +0200 Subject: [PATCH 01/59] Add class feedback form initial view --- lib/generated/intl/messages_en.dart | 5 + lib/generated/intl/messages_ro.dart | 5 + lib/generated/l10n.dart | 52 ++++++ lib/l10n/intl_en.arb | 5 + lib/l10n/intl_ro.arb | 5 + .../class_feedback/class_feedback_view.dart | 172 ++++++++++++++++++ lib/pages/classes/view/class_view.dart | 13 ++ 7 files changed, 257 insertions(+) create mode 100644 lib/pages/class_feedback/class_feedback_view.dart diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 1840bb7cf..60c26fbbd 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -122,6 +122,7 @@ class MessageLookup extends MessageLookupByLibrary { "infoSelect" : MessageLookupByLibrary.simpleMessage("Select the"), "infoYouNeedToSelect" : MessageLookupByLibrary.simpleMessage("You first need to select the"), "labelAskPermissions" : MessageLookupByLibrary.simpleMessage("Request editing permissions"), + "labelAssistant" : MessageLookupByLibrary.simpleMessage("Assistant"), "labelCategory" : MessageLookupByLibrary.simpleMessage("Category"), "labelClass" : MessageLookupByLibrary.simpleMessage("Class"), "labelColor" : MessageLookupByLibrary.simpleMessage("Color"), @@ -163,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageAccountCreated" : MessageLookupByLibrary.simpleMessage("Account created successfully."), "messageAccountDeleted" : MessageLookupByLibrary.simpleMessage("Account deleted successfully."), "messageAddCustomWebsite" : MessageLookupByLibrary.simpleMessage("Try adding a custom website."), + "messageAgreeFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words."), "messageAgreePermissions" : MessageLookupByLibrary.simpleMessage("I will only upload information that is correct and accurate, to the best of my knowledge. I understand that submitting erroneous or offensive information on purpose will lead to my editing permissions being permanently revoked."), "messageAnnouncedOnMail" : MessageLookupByLibrary.simpleMessage("You will receive a mail confirmation if your request is approved."), "messageAskPermissionToEdit" : MessageLookupByLibrary.simpleMessage("Why do you want edit permissions for ACS UPB Mobile?"), @@ -204,6 +206,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Welcome!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("You can contribute to the app data, but you first need to request permissions."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Ask for permissions"), + "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Feedback form"), "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Class information"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Classes"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Event details"), @@ -222,7 +225,9 @@ class MessageLookup extends MessageLookupByLibrary { "sectionEventsComingUp" : MessageLookupByLibrary.simpleMessage("Events coming up"), "sectionFAQ" : MessageLookupByLibrary.simpleMessage("FAQ"), "sectionFrequentlyAccessedWebsites" : MessageLookupByLibrary.simpleMessage("Favourite websites"), + "sectionGeneralQuestions" : MessageLookupByLibrary.simpleMessage("General questions"), "sectionGrading" : MessageLookupByLibrary.simpleMessage("Grading"), + "sectionPersonalComments" : MessageLookupByLibrary.simpleMessage("Personal comments"), "sectionShortcuts" : MessageLookupByLibrary.simpleMessage("Shortcuts"), "settingsItemDarkMode" : MessageLookupByLibrary.simpleMessage("Dark Mode"), "settingsItemLanguage" : MessageLookupByLibrary.simpleMessage("Language"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 09dde5326..fa368578b 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -122,6 +122,7 @@ class MessageLookup extends MessageLookupByLibrary { "infoSelect" : MessageLookupByLibrary.simpleMessage("Selectați"), "infoYouNeedToSelect" : MessageLookupByLibrary.simpleMessage("Trebuie întâi să selectați"), "labelAskPermissions" : MessageLookupByLibrary.simpleMessage("Cere permisiuni de editare"), + "labelAssistant" : MessageLookupByLibrary.simpleMessage("Asistent"), "labelCategory" : MessageLookupByLibrary.simpleMessage("Categorie"), "labelClass" : MessageLookupByLibrary.simpleMessage("Materie"), "labelColor" : MessageLookupByLibrary.simpleMessage("Culoare"), @@ -163,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageAccountCreated" : MessageLookupByLibrary.simpleMessage("Contul a fost creat cu succes."), "messageAccountDeleted" : MessageLookupByLibrary.simpleMessage("Contul a fost șters cu succes."), "messageAddCustomWebsite" : MessageLookupByLibrary.simpleMessage("Încercați să adăugați un website."), + "messageAgreeFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("Înțeleg că acest sondaj este extrem de important pentru dezvoltarea continuă a procesului educațional și nu voi adresa insulte sau folosi cuvinte obscene."), "messageAgreePermissions" : MessageLookupByLibrary.simpleMessage("Voi încărca doar informații corecte si precise. Înțeleg că încărcarea informațiilor eronate sau ofensatoare în mod intenționat va conduce la blocarea permisiunilor mele permanent."), "messageAnnouncedOnMail" : MessageLookupByLibrary.simpleMessage("Veți primi o confirmare pe mail dacă vi se acceptă cererea."), "messageAskPermissionToEdit" : MessageLookupByLibrary.simpleMessage("De ce dorești să primești permisiuni de editare în ACS UPB Mobile?"), @@ -204,6 +206,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Bine ai venit!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Cere permisiuni"), + "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Formular feedback"), "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Informații materie"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Materii"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Detalii eveniment"), @@ -222,7 +225,9 @@ class MessageLookup extends MessageLookupByLibrary { "sectionEventsComingUp" : MessageLookupByLibrary.simpleMessage("Evenimente următoare"), "sectionFAQ" : MessageLookupByLibrary.simpleMessage("Întrebări frecvente"), "sectionFrequentlyAccessedWebsites" : MessageLookupByLibrary.simpleMessage("Website-uri favorite"), + "sectionGeneralQuestions" : MessageLookupByLibrary.simpleMessage("Întrebări generale"), "sectionGrading" : MessageLookupByLibrary.simpleMessage("Punctaj"), + "sectionPersonalComments" : MessageLookupByLibrary.simpleMessage("Comentarii personale"), "sectionShortcuts" : MessageLookupByLibrary.simpleMessage("Scurtături"), "settingsItemDarkMode" : MessageLookupByLibrary.simpleMessage("Mod Întunecat"), "settingsItemLanguage" : MessageLookupByLibrary.simpleMessage("Limbă"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index a3384f510..cb9d22df2 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -9,6 +9,8 @@ import 'intl/messages_all.dart'; // ************************************************************************** // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values class S { S(); @@ -333,6 +335,16 @@ class S { ); } + /// `Assistant` + String get labelAssistant { + return Intl.message( + 'Assistant', + name: 'labelAssistant', + desc: '', + args: [], + ); + } + /// `Year` String get labelYear { return Intl.message( @@ -543,6 +555,26 @@ class S { ); } + /// `General questions` + String get sectionGeneralQuestions { + return Intl.message( + 'General questions', + name: 'sectionGeneralQuestions', + desc: '', + args: [], + ); + } + + /// `Personal comments` + String get sectionPersonalComments { + return Intl.message( + 'Personal comments', + name: 'sectionPersonalComments', + desc: '', + args: [], + ); + } + /// `Main page` String get shortcutTypeMain { return Intl.message( @@ -1593,6 +1625,16 @@ class S { ); } + /// `Feedback form` + String get navigationClassFeedback { + return Intl.message( + 'Feedback form', + name: 'navigationClassFeedback', + desc: '', + args: [], + ); + } + /// `Show all` String get filterMenuShowAll { return Intl.message( @@ -2333,6 +2375,16 @@ class S { ); } + /// `I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words.` + String get messageAgreeFeedbackPolicy { + return Intl.message( + 'I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words.', + name: 'messageAgreeFeedbackPolicy', + desc: '', + args: [], + ); + } + /// `You can contribute to the app data, but you first need to request permissions.` String get messageYouCanContribute { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 426f3b188..406f4560b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -30,6 +30,7 @@ "labelEnd": "End", "labelClass": "Class", "labelLecturer": "Lecturer", + "labelAssistant": "Assistant", "labelYear": "Year", "labelSemester": "Semester", "labelTeam": "{name} team", @@ -52,6 +53,8 @@ "sectionEventsComingUp": "Events coming up", "sectionFAQ": "FAQ", "sectionGrading": "Grading", + "sectionGeneralQuestions": "General questions", + "sectionPersonalComments": "Personal comments", "shortcutTypeMain": "Main page", "shortcutTypeClassbook": "Classbook", @@ -163,6 +166,7 @@ "navigationEventDetails": "Event details", "navigationNewsFeed": "News feed", "navigationClassInfo": "Class information", + "navigationClassFeedback": "Feedback form", "filterMenuShowAll": "Show all", "filterMenuShowMine": "Show only mine", @@ -244,6 +248,7 @@ "messageChangePasswordSuccess": "Password changed successfully.", "messageChangeEmailSuccess": "Email changed successfully", "messageAgreePermissions": "I will only upload information that is correct and accurate, to the best of my knowledge. I understand that submitting erroneous or offensive information on purpose will lead to my editing permissions being permanently revoked.", + "messageAgreeFeedbackPolicy": "I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words.", "messageYouCanContribute": "You can contribute to the app data, but you first need to request permissions.", "messageThereAreNoEventsForSelected": "There are no events for the selected ", "messagePictureUpdatedSuccess": "Profile picture updated successfully.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 6114c9310..c9c9ceb10 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -30,6 +30,7 @@ "labelEnd": "Sfârșit", "labelClass": "Materie", "labelLecturer": "Profesor", + "labelAssistant": "Asistent", "labelYear": "Anul", "labelSemester": "Semestrul", "labelTeam": "Echipa {name}", @@ -52,6 +53,8 @@ "sectionEventsComingUp": "Evenimente următoare", "sectionFAQ": "Întrebări frecvente", "sectionGrading": "Punctaj", + "sectionGeneralQuestions": "Întrebări generale", + "sectionPersonalComments": "Comentarii personale", "shortcutTypeMain": "Pagina principală", "shortcutTypeClassbook": "Catalog", @@ -163,6 +166,7 @@ "navigationEventDetails": "Detalii eveniment", "navigationNewsFeed": "Știri", "navigationClassInfo": "Informații materie", + "navigationClassFeedback": "Formular feedback", "filterMenuShowAll": "Arată tot", "filterMenuShowMine": "Arată doar pe ale mele", @@ -244,6 +248,7 @@ "messageChangePasswordSuccess": "Parola a fost schimbată cu succes.", "messageChangeEmailSuccess": "Email-ul a fost schimbat cu succes", "messageAgreePermissions": "Voi încărca doar informații corecte si precise. Înțeleg că încărcarea informațiilor eronate sau ofensatoare în mod intenționat va conduce la blocarea permisiunilor mele permanent.", + "messageAgreeFeedbackPolicy": "Înțeleg că acest sondaj este extrem de important pentru dezvoltarea continuă a procesului educațional și nu voi adresa insulte sau folosi cuvinte obscene.", "messageYouCanContribute": "Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni.", "messageThereAreNoEventsForSelected": "Nu există evenimente pentru selecția de ", "messagePictureUpdatedSuccess": "Poza a fost actualizată cu succes.", diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart new file mode 100644 index 000000000..7991ae3eb --- /dev/null +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -0,0 +1,172 @@ +import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +import 'package:acs_upb_mobile/widgets/scaffold.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:acs_upb_mobile/generated/l10n.dart'; + +class ClassFeedbackView extends StatefulWidget { + const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); + + final ClassHeader classHeader; + + @override + _ClassFeedbackViewState createState() => _ClassFeedbackViewState(); +} + +class _ClassFeedbackViewState extends State { + final formKey = GlobalKey(); + TextEditingController classController; + bool agreedToResponsibilities = false; + + @override + void initState() { + super.initState(); + + classController = + TextEditingController(text: widget.classHeader?.id ?? ''); + } + + @override + Widget build(BuildContext context) { + final personProvider = Provider.of(context); + + return AppScaffold( + title: Text(S.of(context).navigationClassFeedback), + actions: [_submitButton()], + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + TextFormField( + enabled: false, + controller: classController, + decoration: InputDecoration( + labelText: S.of(context).labelClass, + prefixIcon: const Icon(Icons.class_), + ), + onChanged: (_) => setState(() {}), + ), + FutureBuilder( + future: personProvider + .mostRecentLecturer(widget.classHeader.id), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + final lecturerName = snapshot.data; + return TextFormField( + enabled: false, + controller: TextEditingController( + text: lecturerName ?? '-'), + decoration: InputDecoration( + labelText: S.of(context).labelLecturer, + prefixIcon: const Icon(Icons.person), + ), + onChanged: (_) => setState(() {}), + ); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + ), + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelAssistant, + prefixIcon: const Icon(Icons.person), + ), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + Text( + S.of(context).sectionGeneralQuestions, + style: Theme.of(context).textTheme.headline6, + ), + TextFormField( + enabled: false, + controller: + TextEditingController(text: 'First question'), + ), + TextFormField( + enabled: false, + controller: + TextEditingController(text: 'First question'), + ), + TextFormField( + enabled: false, + controller: + TextEditingController(text: 'First question'), + ), + TextFormField( + enabled: false, + controller: + TextEditingController(text: 'First question'), + ), + const SizedBox(height: 24), + Text( + S.of(context).sectionPersonalComments, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: agreedToResponsibilities, + visualDensity: VisualDensity.compact, + onChanged: (value) => + setState(() => agreedToResponsibilities = value), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10.25), + child: Text( + S.of(context).messageAgreeFeedbackPolicy, + style: Theme.of(context).textTheme.subtitle1, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + AppScaffoldAction _submitButton() => AppScaffoldAction( + text: 'Submit', + onPressed: () async { + if (!formKey.currentState.validate()) return; + }, + ); +} diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index ba687e22f..7c11f93df 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -1,5 +1,6 @@ import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/class_feedback_view.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; @@ -37,6 +38,18 @@ class _ClassViewState extends State { return AppScaffold( title: Text(S.of(context).navigationClassInfo), + actions: [ + AppScaffoldAction( + icon: Icons.comment, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + ClassFeedbackView(classHeader: widget.classHeader), + ), + ); + }), + ], body: FutureBuilder( future: classProvider.fetchClassInfo(widget.classHeader, context: context), From 9b6f03266470c336c484e0aa740205f65dfdfcae Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 28 Mar 2021 21:30:03 +0300 Subject: [PATCH 02/59] Add emoji radio bar for answering questions --- lib/generated/intl/messages_en.dart | 24 ++ lib/generated/intl/messages_ro.dart | 24 ++ lib/generated/l10n.dart | 240 ++++++++++++ lib/l10n/intl_en.arb | 25 ++ lib/l10n/intl_ro.arb | 25 ++ .../class_feedback/class_feedback_view.dart | 370 +++++++++++++----- lib/pages/class_feedback/form_field.dart | 75 ++++ lib/widgets/radio_emoji.dart | 98 +++++ 8 files changed, 779 insertions(+), 102 deletions(-) create mode 100644 lib/pages/class_feedback/form_field.dart create mode 100644 lib/widgets/radio_emoji.dart diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 60c26fbbd..e41952347 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -95,6 +95,27 @@ class MessageLookup extends MessageLookupByLibrary { "errorPictureSizeToBig" : MessageLookupByLibrary.simpleMessage("Please select a picture that is less than 5MB."), "errorSomethingWentWrong" : MessageLookupByLibrary.simpleMessage("Something went wrong."), "errorTooManyRequests" : MessageLookupByLibrary.simpleMessage("There have been too many requests from this device."), + "feedbackActivitiesQuestion" : MessageLookupByLibrary.simpleMessage("Approximate number of activities that you attended (lectures + applications):"), + "feedbackApplicationsQuestion1" : MessageLookupByLibrary.simpleMessage("Does the assistant master the field of study well?"), + "feedbackApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Did the applications stimulate discussions and did the assistant clearly answer students\' questions?"), + "feedbackApplicationsQuestion3" : MessageLookupByLibrary.simpleMessage("Was the assistant\'s behaviour towards students appropriate?"), + "feedbackApplicationsQuestion4" : MessageLookupByLibrary.simpleMessage("Are the teaching materials provided sufficient to understand the applications?"), + "feedbackGeneralQuestion1" : MessageLookupByLibrary.simpleMessage("Is your overall assessment of this discipline positive?"), + "feedbackGeneralQuestion2" : MessageLookupByLibrary.simpleMessage("What grade do you expect to get in this class? (1-10)"), + "feedbackGeneralQuestion3" : MessageLookupByLibrary.simpleMessage("Is the overall load in this class lower than in other classes that offer the same number of credits?"), + "feedbackGeneralQuestion4" : MessageLookupByLibrary.simpleMessage("Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?"), + "feedbackHomeworkQuestion1" : MessageLookupByLibrary.simpleMessage("Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours)."), + "feedbackHomeworkQuestion2" : MessageLookupByLibrary.simpleMessage("Were the number and difficulty of the homework adequate?"), + "feedbackHomeworkQuestion3" : MessageLookupByLibrary.simpleMessage("Did the homework / projects / practical activities help to understand the class?"), + "feedbackLectureApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Was the exposure method appropriate?"), + "feedbackLectureQuestion1" : MessageLookupByLibrary.simpleMessage("Does the teacher master the field of study well?"), + "feedbackLectureQuestion3" : MessageLookupByLibrary.simpleMessage("Did the lecture stimulate discussions and did the teacher clearly answer students\' questions?"), + "feedbackLectureQuestion4" : MessageLookupByLibrary.simpleMessage("Was the teacher\'s behaviour towards students appropriate?"), + "feedbackLectureQuestion5" : MessageLookupByLibrary.simpleMessage("Are the teaching materials provided sufficient to understand the lecture?"), + "feedbackPersonalQuestion1" : MessageLookupByLibrary.simpleMessage("What are the positive aspects of this class?"), + "feedbackPersonalQuestion2" : MessageLookupByLibrary.simpleMessage("What do you think needs to be improved in this class?"), + "feedbackPersonalQuestion3" : MessageLookupByLibrary.simpleMessage("In your opinion, the main difficulty in pursuing this class comes from:"), + "feedbackPersonalQuestion4" : MessageLookupByLibrary.simpleMessage("Other personal comments or suggestions regarding the activities carried out in this discipline:"), "fileAcsBanner" : MessageLookupByLibrary.simpleMessage("assets/images/acs_banner_en.png"), "filterMenuRelevance" : MessageLookupByLibrary.simpleMessage("Filter by relevance"), "filterMenuShowAll" : MessageLookupByLibrary.simpleMessage("Show all"), @@ -136,6 +157,7 @@ class MessageLookup extends MessageLookupByLibrary { "labelEvaluation" : MessageLookupByLibrary.simpleMessage("Evaluation"), "labelEven" : MessageLookupByLibrary.simpleMessage("Even"), "labelFirstName" : MessageLookupByLibrary.simpleMessage("First name"), + "labelGrade" : MessageLookupByLibrary.simpleMessage("Grade"), "labelLastName" : MessageLookupByLibrary.simpleMessage("Last name"), "labelLastUpdated" : MessageLookupByLibrary.simpleMessage("Last updated"), "labelLecturer" : MessageLookupByLibrary.simpleMessage("Lecturer"), @@ -221,12 +243,14 @@ class MessageLookup extends MessageLookupByLibrary { "navigationTimetable" : MessageLookupByLibrary.simpleMessage("Timetable"), "relevanceAnyone" : MessageLookupByLibrary.simpleMessage("Anyone"), "relevanceOnlyMe" : MessageLookupByLibrary.simpleMessage("Only me"), + "sectionApplications" : MessageLookupByLibrary.simpleMessage("Applications"), "sectionEvents" : MessageLookupByLibrary.simpleMessage("Events"), "sectionEventsComingUp" : MessageLookupByLibrary.simpleMessage("Events coming up"), "sectionFAQ" : MessageLookupByLibrary.simpleMessage("FAQ"), "sectionFrequentlyAccessedWebsites" : MessageLookupByLibrary.simpleMessage("Favourite websites"), "sectionGeneralQuestions" : MessageLookupByLibrary.simpleMessage("General questions"), "sectionGrading" : MessageLookupByLibrary.simpleMessage("Grading"), + "sectionInvolvement" : MessageLookupByLibrary.simpleMessage("Involvement"), "sectionPersonalComments" : MessageLookupByLibrary.simpleMessage("Personal comments"), "sectionShortcuts" : MessageLookupByLibrary.simpleMessage("Shortcuts"), "settingsItemDarkMode" : MessageLookupByLibrary.simpleMessage("Dark Mode"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index fa368578b..985685e32 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -95,6 +95,27 @@ class MessageLookup extends MessageLookupByLibrary { "errorPictureSizeToBig" : MessageLookupByLibrary.simpleMessage("Selectați o fotografie care are mai puțin de 5MB."), "errorSomethingWentWrong" : MessageLookupByLibrary.simpleMessage("A apărut o problemă."), "errorTooManyRequests" : MessageLookupByLibrary.simpleMessage("Au fost trimise prea multe cereri de pe acest dispozitiv."), + "feedbackActivitiesQuestion" : MessageLookupByLibrary.simpleMessage("Numărul aproximativ de activități la care ați participat (curs + aplicații):"), + "feedbackApplicationsQuestion1" : MessageLookupByLibrary.simpleMessage("Cadrul didactic stăpânește bine domeniul de studiu?"), + "feedbackApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Aplicațiile au stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?"), + "feedbackApplicationsQuestion3" : MessageLookupByLibrary.simpleMessage("Comportamentul cadrului didactic față de studenți a fost adecvat?"), + "feedbackApplicationsQuestion4" : MessageLookupByLibrary.simpleMessage("Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea aplicațiilor?"), + "feedbackGeneralQuestion1" : MessageLookupByLibrary.simpleMessage("Evaluarea dumneavoastră generală cu privire la această disciplină este pozitivă?"), + "feedbackGeneralQuestion2" : MessageLookupByLibrary.simpleMessage("Care este nota pe care vă așteptați să o obțineți la această disciplină? (1-10)"), + "feedbackGeneralQuestion3" : MessageLookupByLibrary.simpleMessage("Încărcarea generală la această disciplină este mai mică decât cea a altor discipline care oferă același număr de credite?"), + "feedbackGeneralQuestion4" : MessageLookupByLibrary.simpleMessage("Dotarea (locație/echipamente hardware și software/suport digital) este adecvată activităților acestei discipline?"), + "feedbackHomeworkQuestion1" : MessageLookupByLibrary.simpleMessage("Estimați numărul mediu de ore pe săptămână dedicate rezolvării temelor (un număr între 1 și 10 ore)."), + "feedbackHomeworkQuestion2" : MessageLookupByLibrary.simpleMessage("Numărul și dificultatea temelor au fost adecvate?"), + "feedbackHomeworkQuestion3" : MessageLookupByLibrary.simpleMessage("Temele/proiectele/activitățile practice au ajutat la înțelegerea materiei?"), + "feedbackLectureApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Metoda de expunere a fost potrivită?"), + "feedbackLectureQuestion1" : MessageLookupByLibrary.simpleMessage("Cadrul didactic stăpânește bine domeniul de studiu?"), + "feedbackLectureQuestion3" : MessageLookupByLibrary.simpleMessage("Cursul a stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?"), + "feedbackLectureQuestion4" : MessageLookupByLibrary.simpleMessage("Comportamentul cadrului didactic față de studenți a fost adecvat?"), + "feedbackLectureQuestion5" : MessageLookupByLibrary.simpleMessage("Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea cursului?"), + "feedbackPersonalQuestion1" : MessageLookupByLibrary.simpleMessage("Care sunt aspectele pozitive ale acestei discipline?"), + "feedbackPersonalQuestion2" : MessageLookupByLibrary.simpleMessage("Ce considerați că trebuie îmbunătățit la această disciplină?"), + "feedbackPersonalQuestion3" : MessageLookupByLibrary.simpleMessage("După părerea dumneavoastră, dificultatea principală în urmărirea acestei discipline provine din:"), + "feedbackPersonalQuestion4" : MessageLookupByLibrary.simpleMessage("Alte comentarii personale sau sugestii referitoare la activitățile desfășurate la această disciplină:"), "fileAcsBanner" : MessageLookupByLibrary.simpleMessage("assets/images/acs_banner_ro.png"), "filterMenuRelevance" : MessageLookupByLibrary.simpleMessage("Filtrează după relevanță"), "filterMenuShowAll" : MessageLookupByLibrary.simpleMessage("Arată tot"), @@ -136,6 +157,7 @@ class MessageLookup extends MessageLookupByLibrary { "labelEvaluation" : MessageLookupByLibrary.simpleMessage("Evaluare"), "labelEven" : MessageLookupByLibrary.simpleMessage("Pară"), "labelFirstName" : MessageLookupByLibrary.simpleMessage("Prenume"), + "labelGrade" : MessageLookupByLibrary.simpleMessage("Notă"), "labelLastName" : MessageLookupByLibrary.simpleMessage("Nume"), "labelLastUpdated" : MessageLookupByLibrary.simpleMessage("Ultima modificare"), "labelLecturer" : MessageLookupByLibrary.simpleMessage("Profesor"), @@ -221,12 +243,14 @@ class MessageLookup extends MessageLookupByLibrary { "navigationTimetable" : MessageLookupByLibrary.simpleMessage("Orar"), "relevanceAnyone" : MessageLookupByLibrary.simpleMessage("Oricine"), "relevanceOnlyMe" : MessageLookupByLibrary.simpleMessage("Doar eu"), + "sectionApplications" : MessageLookupByLibrary.simpleMessage("Aplicații"), "sectionEvents" : MessageLookupByLibrary.simpleMessage("Evenimente"), "sectionEventsComingUp" : MessageLookupByLibrary.simpleMessage("Evenimente următoare"), "sectionFAQ" : MessageLookupByLibrary.simpleMessage("Întrebări frecvente"), "sectionFrequentlyAccessedWebsites" : MessageLookupByLibrary.simpleMessage("Website-uri favorite"), "sectionGeneralQuestions" : MessageLookupByLibrary.simpleMessage("Întrebări generale"), "sectionGrading" : MessageLookupByLibrary.simpleMessage("Punctaj"), + "sectionInvolvement" : MessageLookupByLibrary.simpleMessage("Implicare"), "sectionPersonalComments" : MessageLookupByLibrary.simpleMessage("Comentarii personale"), "sectionShortcuts" : MessageLookupByLibrary.simpleMessage("Scurtături"), "settingsItemDarkMode" : MessageLookupByLibrary.simpleMessage("Mod Întunecat"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index cb9d22df2..8173b93fc 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -405,6 +405,16 @@ class S { ); } + /// `Grade` + String get labelGrade { + return Intl.message( + 'Grade', + name: 'labelGrade', + desc: '', + args: [], + ); + } + /// `Privacy Policy` String get labelPrivacyPolicy { return Intl.message( @@ -575,6 +585,26 @@ class S { ); } + /// `Applications` + String get sectionApplications { + return Intl.message( + 'Applications', + name: 'sectionApplications', + desc: '', + args: [], + ); + } + + /// `Involvement` + String get sectionInvolvement { + return Intl.message( + 'Involvement', + name: 'sectionInvolvement', + desc: '', + args: [], + ); + } + /// `Main page` String get shortcutTypeMain { return Intl.message( @@ -2525,6 +2555,216 @@ class S { ); } + /// `Is your overall assessment of this discipline positive?` + String get feedbackGeneralQuestion1 { + return Intl.message( + 'Is your overall assessment of this discipline positive?', + name: 'feedbackGeneralQuestion1', + desc: '', + args: [], + ); + } + + /// `What grade do you expect to get in this class? (1-10)` + String get feedbackGeneralQuestion2 { + return Intl.message( + 'What grade do you expect to get in this class? (1-10)', + name: 'feedbackGeneralQuestion2', + desc: '', + args: [], + ); + } + + /// `Is the overall load in this class lower than in other classes that offer the same number of credits?` + String get feedbackGeneralQuestion3 { + return Intl.message( + 'Is the overall load in this class lower than in other classes that offer the same number of credits?', + name: 'feedbackGeneralQuestion3', + desc: '', + args: [], + ); + } + + /// `Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?` + String get feedbackGeneralQuestion4 { + return Intl.message( + 'Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?', + name: 'feedbackGeneralQuestion4', + desc: '', + args: [], + ); + } + + /// `Approximate number of activities that you attended (lectures + applications):` + String get feedbackActivitiesQuestion { + return Intl.message( + 'Approximate number of activities that you attended (lectures + applications):', + name: 'feedbackActivitiesQuestion', + desc: '', + args: [], + ); + } + + /// `Does the teacher master the field of study well?` + String get feedbackLectureQuestion1 { + return Intl.message( + 'Does the teacher master the field of study well?', + name: 'feedbackLectureQuestion1', + desc: '', + args: [], + ); + } + + /// `Was the exposure method appropriate?` + String get feedbackLectureApplicationsQuestion2 { + return Intl.message( + 'Was the exposure method appropriate?', + name: 'feedbackLectureApplicationsQuestion2', + desc: '', + args: [], + ); + } + + /// `Did the lecture stimulate discussions and did the teacher clearly answer students' questions?` + String get feedbackLectureQuestion3 { + return Intl.message( + 'Did the lecture stimulate discussions and did the teacher clearly answer students\' questions?', + name: 'feedbackLectureQuestion3', + desc: '', + args: [], + ); + } + + /// `Was the teacher's behaviour towards students appropriate?` + String get feedbackLectureQuestion4 { + return Intl.message( + 'Was the teacher\'s behaviour towards students appropriate?', + name: 'feedbackLectureQuestion4', + desc: '', + args: [], + ); + } + + /// `Are the teaching materials provided sufficient to understand the lecture?` + String get feedbackLectureQuestion5 { + return Intl.message( + 'Are the teaching materials provided sufficient to understand the lecture?', + name: 'feedbackLectureQuestion5', + desc: '', + args: [], + ); + } + + /// `Does the assistant master the field of study well?` + String get feedbackApplicationsQuestion1 { + return Intl.message( + 'Does the assistant master the field of study well?', + name: 'feedbackApplicationsQuestion1', + desc: '', + args: [], + ); + } + + /// `Did the applications stimulate discussions and did the assistant clearly answer students' questions?` + String get feedbackApplicationsQuestion2 { + return Intl.message( + 'Did the applications stimulate discussions and did the assistant clearly answer students\' questions?', + name: 'feedbackApplicationsQuestion2', + desc: '', + args: [], + ); + } + + /// `Was the assistant's behaviour towards students appropriate?` + String get feedbackApplicationsQuestion3 { + return Intl.message( + 'Was the assistant\'s behaviour towards students appropriate?', + name: 'feedbackApplicationsQuestion3', + desc: '', + args: [], + ); + } + + /// `Are the teaching materials provided sufficient to understand the applications?` + String get feedbackApplicationsQuestion4 { + return Intl.message( + 'Are the teaching materials provided sufficient to understand the applications?', + name: 'feedbackApplicationsQuestion4', + desc: '', + args: [], + ); + } + + /// `Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours).` + String get feedbackHomeworkQuestion1 { + return Intl.message( + 'Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours).', + name: 'feedbackHomeworkQuestion1', + desc: '', + args: [], + ); + } + + /// `Were the number and difficulty of the homework adequate?` + String get feedbackHomeworkQuestion2 { + return Intl.message( + 'Were the number and difficulty of the homework adequate?', + name: 'feedbackHomeworkQuestion2', + desc: '', + args: [], + ); + } + + /// `Did the homework / projects / practical activities help to understand the class?` + String get feedbackHomeworkQuestion3 { + return Intl.message( + 'Did the homework / projects / practical activities help to understand the class?', + name: 'feedbackHomeworkQuestion3', + desc: '', + args: [], + ); + } + + /// `What are the positive aspects of this class?` + String get feedbackPersonalQuestion1 { + return Intl.message( + 'What are the positive aspects of this class?', + name: 'feedbackPersonalQuestion1', + desc: '', + args: [], + ); + } + + /// `What do you think needs to be improved in this class?` + String get feedbackPersonalQuestion2 { + return Intl.message( + 'What do you think needs to be improved in this class?', + name: 'feedbackPersonalQuestion2', + desc: '', + args: [], + ); + } + + /// `In your opinion, the main difficulty in pursuing this class comes from:` + String get feedbackPersonalQuestion3 { + return Intl.message( + 'In your opinion, the main difficulty in pursuing this class comes from:', + name: 'feedbackPersonalQuestion3', + desc: '', + args: [], + ); + } + + /// `Other personal comments or suggestions regarding the activities carried out in this discipline:` + String get feedbackPersonalQuestion4 { + return Intl.message( + 'Other personal comments or suggestions regarding the activities carried out in this discipline:', + name: 'feedbackPersonalQuestion4', + desc: '', + args: [], + ); + } + /// `@stud.acs.upb.ro` String get stringEmailDomain { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 406f4560b..8933b0c90 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -37,6 +37,7 @@ "labelUnknown": "Unknown", "labelEvaluation": "Evaluation", "labelPoints": "Points", + "labelGrade": "Grade", "labelPrivacyPolicy": "Privacy Policy", "labelPersonalInformation": "Personal information", "labelPermissionsConsent": "consent for editing rights", @@ -55,6 +56,8 @@ "sectionGrading": "Grading", "sectionGeneralQuestions": "General questions", "sectionPersonalComments": "Personal comments", + "sectionApplications": "Applications", + "sectionInvolvement": "Involvement", "shortcutTypeMain": "Main page", "shortcutTypeClassbook": "Classbook", @@ -265,6 +268,28 @@ "infoAppIsOpenSource": "ACS UPB Mobile is open source.", "infoEmail": "This is the same username you use to log in to {forum}.", + "feedbackGeneralQuestion1": "Is your overall assessment of this discipline positive?", + "feedbackGeneralQuestion2": "What grade do you expect to get in this class? (1-10)", + "feedbackGeneralQuestion3": "Is the overall load in this class lower than in other classes that offer the same number of credits?", + "feedbackGeneralQuestion4": "Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?", + "feedbackActivitiesQuestion": "Approximate number of activities that you attended (lectures + applications):", + "feedbackLectureQuestion1": "Does the teacher master the field of study well?", + "feedbackLectureApplicationsQuestion2": "Was the exposure method appropriate?", + "feedbackLectureQuestion3": "Did the lecture stimulate discussions and did the teacher clearly answer students' questions?", + "feedbackLectureQuestion4": "Was the teacher's behaviour towards students appropriate?", + "feedbackLectureQuestion5": "Are the teaching materials provided sufficient to understand the lecture?", + "feedbackApplicationsQuestion1": "Does the assistant master the field of study well?", + "feedbackApplicationsQuestion2": "Did the applications stimulate discussions and did the assistant clearly answer students' questions?", + "feedbackApplicationsQuestion3": "Was the assistant's behaviour towards students appropriate?", + "feedbackApplicationsQuestion4": "Are the teaching materials provided sufficient to understand the applications?", + "feedbackHomeworkQuestion1": "Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours).", + "feedbackHomeworkQuestion2": "Were the number and difficulty of the homework adequate?", + "feedbackHomeworkQuestion3": "Did the homework / projects / practical activities help to understand the class?", + "feedbackPersonalQuestion1": "What are the positive aspects of this class?", + "feedbackPersonalQuestion2": "What do you think needs to be improved in this class?", + "feedbackPersonalQuestion3": "In your opinion, the main difficulty in pursuing this class comes from:", + "feedbackPersonalQuestion4": "Other personal comments or suggestions regarding the activities carried out in this discipline:", + "stringEmailDomain": "@stud.acs.upb.ro", "stringForum": "cs.curs.pub.ro", "stringAnonymous": "Anonymous", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index c9c9ceb10..c2bbda01f 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -37,6 +37,7 @@ "labelUnknown": "Necunoscut", "labelEvaluation": "Evaluare", "labelPoints": "Puncte", + "labelGrade": "Notă", "labelPrivacyPolicy": "Privacy Policy", "labelPersonalInformation": "Informații personale", "labelPermissionsConsent": "consimțământul pentru drepturi de editare", @@ -55,6 +56,8 @@ "sectionGrading": "Punctaj", "sectionGeneralQuestions": "Întrebări generale", "sectionPersonalComments": "Comentarii personale", + "sectionApplications": "Aplicații", + "sectionInvolvement": "Implicare", "shortcutTypeMain": "Pagina principală", "shortcutTypeClassbook": "Catalog", @@ -266,6 +269,28 @@ "infoAppIsOpenSource": "ACS UPB Mobile este open source.", "infoEmail": "Acesta este același username pe care îl folosești să te loghezi pe {forum}.", + "feedbackGeneralQuestion1": "Evaluarea dumneavoastră generală cu privire la această disciplină este pozitivă?", + "feedbackGeneralQuestion2": "Care este nota pe care vă așteptați să o obțineți la această disciplină? (1-10)", + "feedbackGeneralQuestion3": "Încărcarea generală la această disciplină este mai mică decât cea a altor discipline care oferă același număr de credite?", + "feedbackGeneralQuestion4": "Dotarea (locație/echipamente hardware și software/suport digital) este adecvată activităților acestei discipline?", + "feedbackActivitiesQuestion": "Numărul aproximativ de activități la care ați participat (curs + aplicații):", + "feedbackLectureQuestion1": "Cadrul didactic stăpânește bine domeniul de studiu?", + "feedbackLectureApplicationsQuestion2": "Metoda de expunere a fost potrivită?", + "feedbackLectureQuestion3": "Cursul a stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?", + "feedbackLectureQuestion4": "Comportamentul cadrului didactic față de studenți a fost adecvat?", + "feedbackLectureQuestion5": "Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea cursului?", + "feedbackApplicationsQuestion1": "Cadrul didactic stăpânește bine domeniul de studiu?", + "feedbackApplicationsQuestion2": "Aplicațiile au stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?", + "feedbackApplicationsQuestion3": "Comportamentul cadrului didactic față de studenți a fost adecvat?", + "feedbackApplicationsQuestion4": "Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea aplicațiilor?", + "feedbackHomeworkQuestion1": "Estimați numărul mediu de ore pe săptămână dedicate rezolvării temelor (un număr între 1 și 10 ore).", + "feedbackHomeworkQuestion2": "Numărul și dificultatea temelor au fost adecvate?", + "feedbackHomeworkQuestion3": "Temele/proiectele/activitățile practice au ajutat la înțelegerea materiei?", + "feedbackPersonalQuestion1": "Care sunt aspectele pozitive ale acestei discipline?", + "feedbackPersonalQuestion2": "Ce considerați că trebuie îmbunătățit la această disciplină?", + "feedbackPersonalQuestion3": "După părerea dumneavoastră, dificultatea principală în urmărirea acestei discipline provine din:", + "feedbackPersonalQuestion4": "Alte comentarii personale sau sugestii referitoare la activitățile desfășurate la această disciplină:", + "stringEmailDomain": "@stud.acs.upb.ro", "stringForum": "cs.curs.pub.ro", "stringAnonymous": "Anonim", diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 7991ae3eb..3de9a0a63 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'form_field.dart'; + class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -20,18 +22,47 @@ class _ClassFeedbackViewState extends State { TextEditingController classController; bool agreedToResponsibilities = false; + List _feedbackValue = []; + List _isFormFieldComplete = []; + List involvementPercentages = []; + String selectedInvolvement; + @override void initState() { super.initState(); - classController = - TextEditingController(text: widget.classHeader?.id ?? ''); + classController = TextEditingController(text: widget.classHeader?.id ?? ''); + involvementPercentages = [ + '0% ... 20%', + '20% ... 40%', + '40% ... 60%', + '60% ... 80%', + '80% ... 100%' + ]; + } + + void _handleRadioButton(int group, int value) { + setState(() { + _feedbackValue[group] = value; + _isFormFieldComplete[group] = false; + }); } @override Widget build(BuildContext context) { final personProvider = Provider.of(context); + final generalQuestions = [ + S.of(context).feedbackGeneralQuestion1, + S.of(context).feedbackGeneralQuestion3, + S.of(context).feedbackGeneralQuestion4 + ]; + + for (int i = 0; i < generalQuestions.length; ++i) { + _feedbackValue.add(-1); + _isFormFieldComplete.add(false); + } + return AppScaffold( title: Text(S.of(context).navigationClassFeedback), actions: [_submitButton()], @@ -45,115 +76,250 @@ class _ClassFeedbackViewState extends State { mainAxisSize: MainAxisSize.min, children: [ Column( - children: [ - TextFormField( - enabled: false, - controller: classController, - decoration: InputDecoration( - labelText: S.of(context).labelClass, - prefixIcon: const Icon(Icons.class_), - ), - onChanged: (_) => setState(() {}), - ), - FutureBuilder( - future: personProvider - .mostRecentLecturer(widget.classHeader.id), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done) { - final lecturerName = snapshot.data; - return TextFormField( - enabled: false, - controller: TextEditingController( - text: lecturerName ?? '-'), - decoration: InputDecoration( - labelText: S.of(context).labelLecturer, - prefixIcon: const Icon(Icons.person), - ), - onChanged: (_) => setState(() {}), - ); - } else { - return const Center( - child: CircularProgressIndicator()); - } - }, - ), - TextFormField( - decoration: InputDecoration( - labelText: S.of(context).labelAssistant, - prefixIcon: const Icon(Icons.person), - ), - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 24), - Text( - S.of(context).sectionGeneralQuestions, - style: Theme.of(context).textTheme.headline6, - ), - TextFormField( - enabled: false, - controller: - TextEditingController(text: 'First question'), + children: [ + TextFormField( + enabled: false, + controller: classController, + decoration: InputDecoration( + labelText: S.of(context).labelClass, + prefixIcon: const Icon(Icons.class_), ), - TextFormField( - enabled: false, - controller: - TextEditingController(text: 'First question'), - ), - TextFormField( - enabled: false, - controller: - TextEditingController(text: 'First question'), + onChanged: (_) => setState(() {}), + ), + FutureBuilder( + future: personProvider + .mostRecentLecturer(widget.classHeader.id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + final lecturerName = snapshot.data; + return TextFormField( + enabled: false, + controller: TextEditingController( + text: lecturerName ?? '-'), + decoration: InputDecoration( + labelText: S.of(context).labelLecturer, + prefixIcon: const Icon(Icons.person), + ), + onChanged: (_) => setState(() {}), + ); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + ), + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelAssistant, + prefixIcon: const Icon(Icons.person), ), - TextFormField( - enabled: false, - controller: - TextEditingController(text: 'First question'), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + Text( + S.of(context).sectionGeneralQuestions, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackGeneralQuestion2, + style: const TextStyle( + fontSize: 18, ), - const SizedBox(height: 24), - Text( - S.of(context).sectionPersonalComments, - style: Theme.of(context).textTheme.headline6, + ), + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelGrade, + prefixIcon: const Icon(Icons.grade), ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + ] + ..addAll( + generalQuestions.asMap().entries.map((entry) { + return FeedbackFormField( + id: entry.key + 1, + question: entry.value, + groupValue: _feedbackValue[entry.key], + radioHandler: (int value) => + _handleRadioButton(entry.key, value), + error: _isFormFieldComplete[entry.key] + ? S + .of(context) + .warningYouNeedToSelectAtLeastOne + : null, + ); + }), + ) + ..addAll([ + Text( + S.of(context).sectionInvolvement, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackActivitiesQuestion, + style: const TextStyle( + fontSize: 18, + ), + ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.of(context).sectionInvolvement, + prefixIcon: const Icon(Icons.local_activity), + ), + value: selectedInvolvement, + items: involvementPercentages + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => selectedInvolvement = selection); + }, + validator: (selection) { + if (selection == null) { + return S + .of(context) + .errorEventTypeCannotBeEmpty; + } + return null; + }, + ), + const SizedBox(height: 24), + Text( + S.of(context).uniEventTypeLecture, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).sectionApplications, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).uniEventTypeHomework, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).sectionPersonalComments, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion1, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], ), - ], + ), ), - ), - ), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Checkbox( - value: agreedToResponsibilities, - visualDensity: VisualDensity.compact, - onChanged: (value) => - setState(() => agreedToResponsibilities = value), + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion2, + style: const TextStyle( + fontSize: 18, ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 10.25), - child: Text( - S.of(context).messageAgreeFeedbackPolicy, - style: Theme.of(context).textTheme.subtitle1, - ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], ), ), - ], - ), - ), - ], - ), + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion3, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion4, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: agreedToResponsibilities, + visualDensity: VisualDensity.compact, + onChanged: (value) => setState( + () => agreedToResponsibilities = value), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10.25), + child: Text( + S.of(context).messageAgreeFeedbackPolicy, + style: + Theme.of(context).textTheme.subtitle1, + ), + ), + ), + ], + ), + ), + ])), ], ), ), diff --git a/lib/pages/class_feedback/form_field.dart b/lib/pages/class_feedback/form_field.dart new file mode 100644 index 000000000..b0b64d3b6 --- /dev/null +++ b/lib/pages/class_feedback/form_field.dart @@ -0,0 +1,75 @@ +import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; +import 'package:flutter/material.dart'; + +class FeedbackFormField extends StatelessWidget { + const FeedbackFormField( + {@required this.id, + @required this.question, + @required this.groupValue, + @required this.radioHandler, + this.error}); + + /// `id` will be treated as a key and also the row number + final int id; + + /// `question` you want to ask + final String question; + + /// `groupValue` is used to identify if the radio button is selected or not + /// + /// if (groupValue == value) then it means that radio button is selected + final int groupValue; + + /// `error` to be displayed below emojis row + /// + /// mostly used if no option is selected + final String error; + + /// This function that will handle all radio button row values + final Function radioHandler; + + /// Determines the number of radio buttons according to their taste + /// + /// 😠 😕 😐 ☺ 😍 + static final List _radioButtons = [1, 2, 3, 4, 5]; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + '$question', + style: const TextStyle( + fontSize: 18, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _radioButtons.map((value) { + return RadioEmoji( + value: value, + groupValue: groupValue, + onChange: radioHandler, + ); + }).toList(), + ), + const SizedBox( + height: 2, + ), + Visibility( + visible: error != null, + child: Text( + '$error', + style: const TextStyle( + color: Colors.red, + ), + ), + ), + const SizedBox( + height: 10, + ), + ], + ); + } +} diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart new file mode 100644 index 000000000..ec0b8378b --- /dev/null +++ b/lib/widgets/radio_emoji.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +class RadioEmoji extends StatefulWidget { + const RadioEmoji({ + @required this.value, + @required this.groupValue, + @required this.onChange, + }) : assert(value >= 1 && value <= 5); + + /// Rating value between 1 and 5 + final int value; + + /// `groupValue` used to identify the radio button group + final int groupValue; + + /// everytime the value of radio changes this function will trigger + final Function onChange; + + /// Emojis describing feedback status + static final List emojiIndex = ['😠', '😕', '😐', '☺', '😍']; + + @override + _RadioEmojiState createState() => _RadioEmojiState(); +} + +class _RadioEmojiState extends State + with SingleTickerProviderStateMixin { + AnimationController controller; + CurvedAnimation animation; + + String emoji; + bool isSelected; + + // This function will trigger each time the radio emoji button is tapped + void _handleTap() { + widget.onChange(widget.value); + _initializeAnimation(); + } + + void _initializeAnimation() { + controller.forward(); + } + + void _stopAnimation() { + controller.value = 0.0; + } + + @override + void initState() { + super.initState(); + + emoji = RadioEmoji.emojiIndex[widget.value - 1]; + + controller = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + animation = CurvedAnimation( + parent: controller, + curve: Curves.elasticOut, + ); + + controller.addListener(() => setState(() {})); + + isSelected = false; + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + isSelected = widget.value == widget.groupValue; + + if (isSelected == false) _stopAnimation(); + + return GestureDetector( + child: Container( + decoration: BoxDecoration( + border: Border.all(style: BorderStyle.none, width: 1), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Text( + emoji, + style: TextStyle( + fontSize: animation.value * 10 + 20.0, + ), + ), + ), + onTap: _handleTap, + ); + } +} From 794ae0e09ec4c2c20109f44fb45e69e4755281e5 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Tue, 6 Apr 2021 09:47:32 +0300 Subject: [PATCH 03/59] Insert questions into a card widget --- lib/generated/intl/messages_en.dart | 2 +- lib/generated/intl/messages_ro.dart | 2 +- lib/generated/l10n.dart | 25 +- lib/l10n/intl_en.arb | 2 +- lib/l10n/intl_ro.arb | 2 +- .../class_feedback/class_feedback_view.dart | 505 +++++++++++------- lib/pages/class_feedback/form_field.dart | 16 +- lib/pages/classes/view/class_view.dart | 2 +- lib/widgets/radio_emoji.dart | 16 +- 9 files changed, 357 insertions(+), 215 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index c2f7fb3e1..5966fe2d8 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -232,7 +232,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Welcome!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("You can contribute to the app data, but you first need to request permissions."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Ask for permissions"), - "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Feedback form"), + "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Review"), "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Class information"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Classes"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Event details"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 755f119ec..faf62f4c0 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -232,7 +232,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Bine ai venit!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Cere permisiuni"), - "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Formular feedback"), + "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Recenzie"), "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Informații materie"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Materii"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Detalii eveniment"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index a796655fb..82ea26aec 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -14,23 +14,22 @@ import 'intl/messages_all.dart'; class S { S(); - + static S current; - - static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static const AppLocalizationDelegate delegate = + AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); - final localeName = Intl.canonicalizedLocale(name); + final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; S.current = S(); - + return S.current; }); - } + } static S of(BuildContext context) { return Localizations.of(context, S); @@ -1656,10 +1655,10 @@ class S { ); } - /// `Feedback form` + /// `Review` String get navigationClassFeedback { return Intl.message( - 'Feedback form', + 'Review', name: 'navigationClassFeedback', desc: '', args: [], @@ -2829,10 +2828,8 @@ class AppLocalizationDelegate extends LocalizationsDelegate { @override bool isSupported(Locale locale) => _isSupported(locale); - @override Future load(Locale locale) => S.load(locale); - @override bool shouldReload(AppLocalizationDelegate old) => false; @@ -2846,4 +2843,4 @@ class AppLocalizationDelegate extends LocalizationsDelegate { } return false; } -} +} \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 401099a37..fbf85c4e6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -169,7 +169,7 @@ "navigationEventDetails": "Event details", "navigationNewsFeed": "News feed", "navigationClassInfo": "Class information", - "navigationClassFeedback": "Feedback form", + "navigationClassFeedback": "Review", "filterMenuShowAll": "Show all", "filterMenuShowMine": "Show only mine", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 992ea62c6..c965e49d8 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -169,7 +169,7 @@ "navigationEventDetails": "Detalii eveniment", "navigationNewsFeed": "Știri", "navigationClassInfo": "Informații materie", - "navigationClassFeedback": "Formular feedback", + "navigationClassFeedback": "Recenzie", "filterMenuShowAll": "Arată tot", "filterMenuShowMine": "Arată doar pe ale mele", diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 3de9a0a63..f5c0cef30 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -22,8 +22,8 @@ class _ClassFeedbackViewState extends State { TextEditingController classController; bool agreedToResponsibilities = false; - List _feedbackValue = []; - List _isFormFieldComplete = []; + final List _feedbackValue = []; + final List _isFormFieldComplete = []; List involvementPercentages = []; String selectedInvolvement; @@ -58,7 +58,28 @@ class _ClassFeedbackViewState extends State { S.of(context).feedbackGeneralQuestion4 ]; - for (int i = 0; i < generalQuestions.length; ++i) { + final lectureQuestions = [ + S.of(context).feedbackLectureQuestion1, + S.of(context).feedbackLectureApplicationsQuestion2, + S.of(context).feedbackLectureQuestion3, + S.of(context).feedbackLectureQuestion4, + S.of(context).feedbackLectureQuestion5 + ]; + + final applicationsQuestions = [ + S.of(context).feedbackApplicationsQuestion1, + S.of(context).feedbackLectureApplicationsQuestion2, + S.of(context).feedbackApplicationsQuestion2, + S.of(context).feedbackApplicationsQuestion3, + S.of(context).feedbackApplicationsQuestion4, + ]; + + final homeworkQuestions = [ + S.of(context).feedbackHomeworkQuestion2, + S.of(context).feedbackHomeworkQuestion3 + ]; + + for (int i = 0; i < lectureQuestions.length; ++i) { _feedbackValue.add(-1); _isFormFieldComplete.add(false); } @@ -69,14 +90,13 @@ class _ClassFeedbackViewState extends State { body: ListView( children: [ Padding( - padding: const EdgeInsets.only(left: 16, right: 16), + padding: const EdgeInsets.all(10), child: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ - Column( - children: [ + Column(children: [ TextFormField( enabled: false, controller: classController, @@ -116,210 +136,323 @@ class _ClassFeedbackViewState extends State { onChanged: (_) => setState(() {}), ), const SizedBox(height: 24), - Text( - S.of(context).sectionGeneralQuestions, - style: Theme.of(context).textTheme.headline6, + // ignore: inference_failure_on_collection_literal + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionGeneralQuestions, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackGeneralQuestion2, + style: const TextStyle( + fontSize: 18, + ), + ), + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelGrade, + prefixIcon: const Icon(Icons.grade), + ), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + ...generalQuestions.asMap().entries.map((entry) { + return FeedbackFormField( + id: entry.key + 1, + question: entry.value, + groupValue: _feedbackValue[entry.key], + radioHandler: (int value) => + _handleRadioButton(entry.key, value), + error: _isFormFieldComplete[entry.key] + ? S + .of(context) + .warningYouNeedToSelectAtLeastOne + : null, + ); + }), + ], + ), + ), ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackGeneralQuestion2, - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionInvolvement, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackActivitiesQuestion, + style: const TextStyle( + fontSize: 18, + ), + ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.of(context).sectionInvolvement, + prefixIcon: const Icon(Icons.local_activity), + ), + value: selectedInvolvement, + items: involvementPercentages + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => selectedInvolvement = selection); + }, + validator: (selection) { + if (selection == null) { + return S + .of(context) + .errorEventTypeCannotBeEmpty; + } + return null; + }, + ), + ], + ), ), ), - TextFormField( - decoration: InputDecoration( - labelText: S.of(context).labelGrade, - prefixIcon: const Icon(Icons.grade), + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).uniEventTypeLecture, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + ...lectureQuestions.asMap().entries.map((entry) { + return FeedbackFormField( + id: entry.key + 1, + question: entry.value, + groupValue: _feedbackValue[entry.key], + radioHandler: (int value) => + _handleRadioButton(entry.key, value), + error: _isFormFieldComplete[entry.key] + ? S + .of(context) + .warningYouNeedToSelectAtLeastOne + : null, + ); + }), + ], + ), ), - onChanged: (_) => setState(() {}), ), - const SizedBox(height: 24), - ] - ..addAll( - generalQuestions.asMap().entries.map((entry) { - return FeedbackFormField( - id: entry.key + 1, - question: entry.value, - groupValue: _feedbackValue[entry.key], - radioHandler: (int value) => - _handleRadioButton(entry.key, value), - error: _isFormFieldComplete[entry.key] - ? S - .of(context) - .warningYouNeedToSelectAtLeastOne - : null, - ); - }), - ) - ..addAll([ - Text( - S.of(context).sectionInvolvement, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackActivitiesQuestion, - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionApplications, + style: Theme.of(context).textTheme.headline6, ), - ), - DropdownButtonFormField( - decoration: InputDecoration( - labelText: S.of(context).sectionInvolvement, - prefixIcon: const Icon(Icons.local_activity), + const SizedBox(height: 24), + ...applicationsQuestions + .asMap() + .entries + .map((entry) { + return FeedbackFormField( + id: entry.key + 1, + question: entry.value, + groupValue: _feedbackValue[entry.key], + radioHandler: (int value) => + _handleRadioButton(entry.key, value), + error: _isFormFieldComplete[entry.key] + ? S + .of(context) + .warningYouNeedToSelectAtLeastOne + : null, + ); + }), + ], + ), + ), + ), + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).uniEventTypeHomework, + style: Theme.of(context).textTheme.headline6, ), - value: selectedInvolvement, - items: involvementPercentages - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type.toString()), - ), - ) - .toList(), - onChanged: (selection) { - formKey.currentState.validate(); - setState(() => selectedInvolvement = selection); - }, - validator: (selection) { - if (selection == null) { - return S - .of(context) - .errorEventTypeCannotBeEmpty; - } - return null; - }, - ), - const SizedBox(height: 24), - Text( - S.of(context).uniEventTypeLecture, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - S.of(context).sectionApplications, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - S.of(context).uniEventTypeHomework, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - S.of(context).sectionPersonalComments, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion1, - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 24), + Text( + S.of(context).feedbackHomeworkQuestion1, + style: const TextStyle( + fontSize: 18, + ), ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelGrade, + prefixIcon: const Icon(Icons.grade), ), + onChanged: (_) => setState(() {}), ), - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion2, - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 24), + ...homeworkQuestions.asMap().entries.map((entry) { + return FeedbackFormField( + id: entry.key + 1, + question: entry.value, + groupValue: _feedbackValue[entry.key], + radioHandler: (int value) => + _handleRadioButton(entry.key, value), + error: _isFormFieldComplete[entry.key] + ? S + .of(context) + .warningYouNeedToSelectAtLeastOne + : null, + ); + }), + ], + ), + ), + ), + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionPersonalComments, + style: Theme.of(context).textTheme.headline6, ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion1, + style: const TextStyle( + fontSize: 18, ), ), - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion3, - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion2, + style: const TextStyle( + fontSize: 18, ), ), - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion4, - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion3, + style: const TextStyle( + fontSize: 18, ), ), - ), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Checkbox( - value: agreedToResponsibilities, - visualDensity: VisualDensity.compact, - onChanged: (value) => setState( - () => agreedToResponsibilities = value), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 10.25), - child: Text( - S.of(context).messageAgreeFeedbackPolicy, - style: - Theme.of(context).textTheme.subtitle1, + ), + ), + const SizedBox(height: 24), + Text( + S.of(context).feedbackPersonalQuestion4, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: null, ), - ), + ], ), - ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: agreedToResponsibilities, + visualDensity: VisualDensity.compact, + onChanged: (value) => setState( + () => agreedToResponsibilities = value), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10.25), + child: Text( + S.of(context).messageAgreeFeedbackPolicy, + style: Theme.of(context).textTheme.subtitle1, + ), ), ), - ])), + ], + ), + ), + ]), ], ), ), diff --git a/lib/pages/class_feedback/form_field.dart b/lib/pages/class_feedback/form_field.dart index b0b64d3b6..d8e66a933 100644 --- a/lib/pages/class_feedback/form_field.dart +++ b/lib/pages/class_feedback/form_field.dart @@ -1,13 +1,14 @@ import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; +import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class FeedbackFormField extends StatelessWidget { - const FeedbackFormField( - {@required this.id, - @required this.question, - @required this.groupValue, - @required this.radioHandler, - this.error}); + const FeedbackFormField({@required this.id, + @required this.question, + @required this.groupValue, + @required this.radioHandler, + this.emojiController, + this.error}); /// `id` will be treated as a key and also the row number final int id; @@ -33,6 +34,8 @@ class FeedbackFormField extends StatelessWidget { /// 😠 😕 😐 ☺ 😍 static final List _radioButtons = [1, 2, 3, 4, 5]; + final SelectableController emojiController; + @override Widget build(BuildContext context) { return Column( @@ -51,6 +54,7 @@ class FeedbackFormField extends StatelessWidget { value: value, groupValue: groupValue, onChange: radioHandler, + emojiController: emojiController, ); }).toList(), ), diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 010ebe14e..b70e37d07 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -40,7 +40,7 @@ class _ClassViewState extends State { title: Text(S.of(context).navigationClassInfo), actions: [ AppScaffoldAction( - icon: Icons.comment, + icon: Icons.rate_review_outlined, onPressed: () { Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index ec0b8378b..7061781d1 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -1,10 +1,13 @@ +import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class RadioEmoji extends StatefulWidget { + const RadioEmoji({ @required this.value, @required this.groupValue, @required this.onChange, + this.emojiController }) : assert(value >= 1 && value <= 5); /// Rating value between 1 and 5 @@ -19,6 +22,8 @@ class RadioEmoji extends StatefulWidget { /// Emojis describing feedback status static final List emojiIndex = ['😠', '😕', '😐', '☺', '😍']; + final SelectableController emojiController; + @override _RadioEmojiState createState() => _RadioEmojiState(); } @@ -85,10 +90,13 @@ class _RadioEmojiState extends State shape: BoxShape.circle, ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Text( - emoji, - style: TextStyle( - fontSize: animation.value * 10 + 20.0, + child: Container( + height: 35, + child: Text( + emoji, + style: TextStyle( + fontSize: animation.value * 10 + 20.0, + ), ), ), ), From 501b05b4d39d24c395c6d3299fa5409e41d4af1b Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Tue, 6 Apr 2021 09:51:48 +0300 Subject: [PATCH 04/59] Fix formatting --- lib/pages/class_feedback/form_field.dart | 13 +++++++------ lib/widgets/radio_emoji.dart | 13 ++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/pages/class_feedback/form_field.dart b/lib/pages/class_feedback/form_field.dart index d8e66a933..df302c1b5 100644 --- a/lib/pages/class_feedback/form_field.dart +++ b/lib/pages/class_feedback/form_field.dart @@ -3,12 +3,13 @@ import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class FeedbackFormField extends StatelessWidget { - const FeedbackFormField({@required this.id, - @required this.question, - @required this.groupValue, - @required this.radioHandler, - this.emojiController, - this.error}); + const FeedbackFormField( + {@required this.id, + @required this.question, + @required this.groupValue, + @required this.radioHandler, + this.emojiController, + this.error}); /// `id` will be treated as a key and also the row number final int id; diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 7061781d1..b70fa37a6 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -2,13 +2,12 @@ import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class RadioEmoji extends StatefulWidget { - - const RadioEmoji({ - @required this.value, - @required this.groupValue, - @required this.onChange, - this.emojiController - }) : assert(value >= 1 && value <= 5); + const RadioEmoji( + {@required this.value, + @required this.groupValue, + @required this.onChange, + this.emojiController}) + : assert(value >= 1 && value <= 5); /// Rating value between 1 and 5 final int value; From 94c6d7b8bd6c940988c9fee59a650bc649ab440a Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 17 Apr 2021 13:57:46 +0300 Subject: [PATCH 05/59] Initial version of feedback form. --- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ro.dart | 1 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ro.arb | 1 + .../class_feedback/class_feedback_view.dart | 148 ++++++++++-------- lib/pages/class_feedback/form_field.dart | 23 +-- lib/widgets/radio_emoji.dart | 148 +++++++++++++----- 8 files changed, 211 insertions(+), 122 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 5966fe2d8..2d7356671 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -138,6 +138,7 @@ class MessageLookup extends MessageLookupByLibrary { "infoAppIsOpenSource" : m2, "infoClasses" : MessageLookupByLibrary.simpleMessage("classes you are interested in"), "infoEmail" : m3, + "infoFormAnonymous" : MessageLookupByLibrary.simpleMessage("This form is anonymous."), "infoMakeSureGroupIsSelected" : MessageLookupByLibrary.simpleMessage("Make sure your group/subgroup is selected in the"), "infoPassword" : MessageLookupByLibrary.simpleMessage("It must contain lower and uppercase letters, one number and one special character, and have a minimum length of 8."), "infoPasswordResetEmailSent" : MessageLookupByLibrary.simpleMessage("Please check your inbox for the password reset e-mail."), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index faf62f4c0..b15cb71a6 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -138,6 +138,7 @@ class MessageLookup extends MessageLookupByLibrary { "infoAppIsOpenSource" : m2, "infoClasses" : MessageLookupByLibrary.simpleMessage("materiile care vă interesează"), "infoEmail" : m3, + "infoFormAnonymous" : MessageLookupByLibrary.simpleMessage("Acest formular este anonim."), "infoMakeSureGroupIsSelected" : MessageLookupByLibrary.simpleMessage("Asigurați-vă că aveți grupa/semigrupa selectată în"), "infoPassword" : MessageLookupByLibrary.simpleMessage("Aceasta trebuie să conțină majuscule, minuscule și cel puțin un număr sau un simbol, având minimum 8 caractere."), "infoPasswordResetEmailSent" : MessageLookupByLibrary.simpleMessage("Please check your inbox for the password reset e-mail."), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 82ea26aec..edd315983 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2555,6 +2555,16 @@ class S { ); } + /// `This form is anonymous.` + String get infoFormAnonymous { + return Intl.message( + 'This form is anonymous.', + name: 'infoFormAnonymous', + desc: '', + args: [], + ); + } + /// `Is your overall assessment of this discipline positive?` String get feedbackGeneralQuestion1 { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fbf85c4e6..38e9b048b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -267,6 +267,7 @@ "infoPassword": "It must contain lower and uppercase letters, one number and one special character, and have a minimum length of 8.", "infoAppIsOpenSource": "{appName} is open source.", "infoEmail": "This is the same username you use to log in to {forum}.", + "infoFormAnonymous": "This form is anonymous.", "feedbackGeneralQuestion1": "Is your overall assessment of this discipline positive?", "feedbackGeneralQuestion2": "What grade do you expect to get in this class? (1-10)", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index c965e49d8..212b8066e 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -268,6 +268,7 @@ "infoPassword": "Aceasta trebuie să conțină majuscule, minuscule și cel puțin un număr sau un simbol, având minimum 8 caractere.", "infoAppIsOpenSource": "{appName} este open source.", "infoEmail": "Acesta este același username pe care îl folosești să te loghezi pe {forum}.", + "infoFormAnonymous": "Acest formular este anonim.", "feedbackGeneralQuestion1": "Evaluarea dumneavoastră generală cu privire la această disciplină este pozitivă?", "feedbackGeneralQuestion2": "Care este nota pe care vă așteptați să o obțineți la această disciplină? (1-10)", diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index f5c0cef30..921b3ab19 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -1,5 +1,7 @@ import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +import 'package:acs_upb_mobile/widgets/icon_text.dart'; +import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -17,7 +19,8 @@ class ClassFeedbackView extends StatefulWidget { _ClassFeedbackViewState createState() => _ClassFeedbackViewState(); } -class _ClassFeedbackViewState extends State { +class _ClassFeedbackViewState extends State + with TickerProviderStateMixin { final formKey = GlobalKey(); TextEditingController classController; bool agreedToResponsibilities = false; @@ -27,6 +30,17 @@ class _ClassFeedbackViewState extends State { List involvementPercentages = []; String selectedInvolvement; + AnimationController animationController; + CurvedAnimation animation; + + Map emojiSelected = { + 0: false, + 1: false, + 2: false, + 3: false, + 4: false, + }; + @override void initState() { super.initState(); @@ -39,13 +53,16 @@ class _ClassFeedbackViewState extends State { '60% ... 80%', '80% ... 100%' ]; - } - void _handleRadioButton(int group, int value) { - setState(() { - _feedbackValue[group] = value; - _isFormFieldComplete[group] = false; - }); + animationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + animation = CurvedAnimation( + parent: animationController, + curve: Curves.elasticOut, + ); } @override @@ -97,12 +114,15 @@ class _ClassFeedbackViewState extends State { mainAxisSize: MainAxisSize.min, children: [ Column(children: [ + IconText( + icon: Icons.info_outline, + text: S.of(context).infoFormAnonymous), TextFormField( enabled: false, controller: classController, decoration: InputDecoration( labelText: S.of(context).labelClass, - prefixIcon: const Icon(Icons.class_), + prefixIcon: const Icon(Icons.class__outlined), ), onChanged: (_) => setState(() {}), ), @@ -118,7 +138,7 @@ class _ClassFeedbackViewState extends State { text: lecturerName ?? '-'), decoration: InputDecoration( labelText: S.of(context).labelLecturer, - prefixIcon: const Icon(Icons.person), + prefixIcon: const Icon(Icons.person_outline), ), onChanged: (_) => setState(() {}), ); @@ -131,12 +151,34 @@ class _ClassFeedbackViewState extends State { TextFormField( decoration: InputDecoration( labelText: S.of(context).labelAssistant, - prefixIcon: const Icon(Icons.person), + prefixIcon: const Icon(Icons.person_outline), ), onChanged: (_) => setState(() {}), ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: agreedToResponsibilities, + visualDensity: VisualDensity.compact, + onChanged: (value) => setState( + () => agreedToResponsibilities = value), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10.25), + child: Text( + S.of(context).messageAgreeFeedbackPolicy, + style: Theme.of(context).textTheme.subtitle1, + ), + ), + ), + ], + ), + ), const SizedBox(height: 24), - // ignore: inference_failure_on_collection_literal Card( child: Padding( padding: const EdgeInsets.all(15), @@ -156,25 +198,38 @@ class _ClassFeedbackViewState extends State { TextFormField( decoration: InputDecoration( labelText: S.of(context).labelGrade, - prefixIcon: const Icon(Icons.grade), + prefixIcon: const Icon(Icons.grade_outlined), ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 24), - ...generalQuestions.asMap().entries.map((entry) { - return FeedbackFormField( - id: entry.key + 1, - question: entry.value, - groupValue: _feedbackValue[entry.key], - radioHandler: (int value) => - _handleRadioButton(entry.key, value), - error: _isFormFieldComplete[entry.key] - ? S - .of(context) - .warningYouNeedToSelectAtLeastOne - : null, - ); - }), + ...generalQuestions.asMap().entries.map( + (entry) { + return EmojiFormField( + question: entry.value, + handleTap: () { + setState(() { + animationController.forward(); + emojiSelected[entry.key] = true; + animationController + .addListener(() => setState(() {})); + }); + }, + animation: animation, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: emojiSelected, + ); + }, + ), ], ), ), @@ -199,7 +254,8 @@ class _ClassFeedbackViewState extends State { DropdownButtonFormField( decoration: InputDecoration( labelText: S.of(context).sectionInvolvement, - prefixIcon: const Icon(Icons.local_activity), + prefixIcon: + const Icon(Icons.local_activity_outlined), ), value: selectedInvolvement, items: involvementPercentages @@ -240,11 +296,7 @@ class _ClassFeedbackViewState extends State { const SizedBox(height: 24), ...lectureQuestions.asMap().entries.map((entry) { return FeedbackFormField( - id: entry.key + 1, question: entry.value, - groupValue: _feedbackValue[entry.key], - radioHandler: (int value) => - _handleRadioButton(entry.key, value), error: _isFormFieldComplete[entry.key] ? S .of(context) @@ -272,11 +324,7 @@ class _ClassFeedbackViewState extends State { .entries .map((entry) { return FeedbackFormField( - id: entry.key + 1, question: entry.value, - groupValue: _feedbackValue[entry.key], - radioHandler: (int value) => - _handleRadioButton(entry.key, value), error: _isFormFieldComplete[entry.key] ? S .of(context) @@ -308,18 +356,14 @@ class _ClassFeedbackViewState extends State { TextFormField( decoration: InputDecoration( labelText: S.of(context).labelGrade, - prefixIcon: const Icon(Icons.grade), + prefixIcon: const Icon(Icons.grade_outlined), ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 24), ...homeworkQuestions.asMap().entries.map((entry) { return FeedbackFormField( - id: entry.key + 1, question: entry.value, - groupValue: _feedbackValue[entry.key], - radioHandler: (int value) => - _handleRadioButton(entry.key, value), error: _isFormFieldComplete[entry.key] ? S .of(context) @@ -425,33 +469,11 @@ class _ClassFeedbackViewState extends State { ), ), ), + const SizedBox(height: 24), ], ), ), ), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Checkbox( - value: agreedToResponsibilities, - visualDensity: VisualDensity.compact, - onChanged: (value) => setState( - () => agreedToResponsibilities = value), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 10.25), - child: Text( - S.of(context).messageAgreeFeedbackPolicy, - style: Theme.of(context).textTheme.subtitle1, - ), - ), - ), - ], - ), - ), ]), ], ), diff --git a/lib/pages/class_feedback/form_field.dart b/lib/pages/class_feedback/form_field.dart index df302c1b5..8504a64e1 100644 --- a/lib/pages/class_feedback/form_field.dart +++ b/lib/pages/class_feedback/form_field.dart @@ -1,42 +1,24 @@ import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; -import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class FeedbackFormField extends StatelessWidget { const FeedbackFormField( - {@required this.id, - @required this.question, - @required this.groupValue, - @required this.radioHandler, - this.emojiController, + {@required this.question, this.error}); - /// `id` will be treated as a key and also the row number - final int id; - /// `question` you want to ask final String question; - /// `groupValue` is used to identify if the radio button is selected or not - /// - /// if (groupValue == value) then it means that radio button is selected - final int groupValue; - /// `error` to be displayed below emojis row /// /// mostly used if no option is selected final String error; - /// This function that will handle all radio button row values - final Function radioHandler; - /// Determines the number of radio buttons according to their taste /// /// 😠 😕 😐 ☺ 😍 static final List _radioButtons = [1, 2, 3, 4, 5]; - final SelectableController emojiController; - @override Widget build(BuildContext context) { return Column( @@ -53,9 +35,6 @@ class FeedbackFormField extends StatelessWidget { children: _radioButtons.map((value) { return RadioEmoji( value: value, - groupValue: groupValue, - onChange: radioHandler, - emojiController: emojiController, ); }).toList(), ), diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index b70fa37a6..64c4c630e 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -1,28 +1,16 @@ -import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class RadioEmoji extends StatefulWidget { - const RadioEmoji( - {@required this.value, - @required this.groupValue, - @required this.onChange, - this.emojiController}) - : assert(value >= 1 && value <= 5); + const RadioEmoji({ + @required this.value, + }) : assert(value >= 1 && value <= 5); /// Rating value between 1 and 5 final int value; - /// `groupValue` used to identify the radio button group - final int groupValue; - - /// everytime the value of radio changes this function will trigger - final Function onChange; - /// Emojis describing feedback status static final List emojiIndex = ['😠', '😕', '😐', '☺', '😍']; - final SelectableController emojiController; - @override _RadioEmojiState createState() => _RadioEmojiState(); } @@ -33,21 +21,6 @@ class _RadioEmojiState extends State CurvedAnimation animation; String emoji; - bool isSelected; - - // This function will trigger each time the radio emoji button is tapped - void _handleTap() { - widget.onChange(widget.value); - _initializeAnimation(); - } - - void _initializeAnimation() { - controller.forward(); - } - - void _stopAnimation() { - controller.value = 0.0; - } @override void initState() { @@ -66,8 +39,6 @@ class _RadioEmojiState extends State ); controller.addListener(() => setState(() {})); - - isSelected = false; } @override @@ -78,10 +49,6 @@ class _RadioEmojiState extends State @override Widget build(BuildContext context) { - isSelected = widget.value == widget.groupValue; - - if (isSelected == false) _stopAnimation(); - return GestureDetector( child: Container( decoration: BoxDecoration( @@ -99,7 +66,114 @@ class _RadioEmojiState extends State ), ), ), - onTap: _handleTap, + onTap: () { + controller.forward(); + }, ); } } + +class EmojiFormField extends FormField> { + EmojiFormField({ + @required Map initialValues, + @required String question, + @required void Function() handleTap, + @required CurvedAnimation animation, + String Function(Map) validator, + Key key, + }) : super( + key: key, + validator: validator, + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: initialValues, + builder: (state) { + final context = state.context; + final List emojis = [ + Icon( + Icons.sentiment_very_dissatisfied, + color: Colors.red, + size: animation.value * 10 + 20.0, + ), + Icon( + Icons.sentiment_dissatisfied, + color: Colors.redAccent, + size: animation.value * 10 + 20.0, + ), + Icon( + Icons.sentiment_neutral, + color: Colors.amber, + size: animation.value * 10 + 20.0, + ), + Icon( + Icons.sentiment_satisfied, + color: Colors.lightGreen, + size: animation.value * 10 + 20.0, + ), + Icon( + Icons.sentiment_very_satisfied, + color: Colors.green, + size: animation.value * 10 + 20.0, + ) + ]; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + '$question', + style: const TextStyle( + fontSize: 18, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + alignment: Alignment.center, + height: 45, + child: ListView.builder( + shrinkWrap: true, + itemCount: emojis.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: handleTap, + child: Container( + decoration: BoxDecoration( + border: Border.all( + style: BorderStyle.none, width: 1), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + child: emojis[index], + ), + ), + const SizedBox(width: 8), + ], + ); + }, + ), + ), + ), + if (state.hasError) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + state.errorText, + style: Theme.of(context) + .textTheme + .caption + .copyWith(color: Theme.of(context).errorColor), + ), + ), + ], + ), + ], + ); + }, + ); +} From 36ed86b7d9facbb2d863133f66015ddcd61b0767 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 17 Apr 2021 21:13:40 +0300 Subject: [PATCH 06/59] Use AnimatedContainer for emoji form field --- .../class_feedback/class_feedback_view.dart | 57 +++--- lib/pages/class_feedback/form_field.dart | 118 ++++++------ lib/widgets/radio_emoji.dart | 173 ++++++++++++------ lib/widgets/selectable.dart | 18 +- 4 files changed, 213 insertions(+), 153 deletions(-) diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 921b3ab19..87f6a6c19 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -207,15 +207,6 @@ class _ClassFeedbackViewState extends State (entry) { return EmojiFormField( question: entry.value, - handleTap: () { - setState(() { - animationController.forward(); - emojiSelected[entry.key] = true; - animationController - .addListener(() => setState(() {})); - }); - }, - animation: animation, validator: (selection) { if (selection.values .where((e) => e != false) @@ -295,13 +286,19 @@ class _ClassFeedbackViewState extends State ), const SizedBox(height: 24), ...lectureQuestions.asMap().entries.map((entry) { - return FeedbackFormField( + return EmojiFormField( question: entry.value, - error: _isFormFieldComplete[entry.key] - ? S + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S .of(context) - .warningYouNeedToSelectAtLeastOne - : null, + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: emojiSelected, ); }), ], @@ -323,13 +320,19 @@ class _ClassFeedbackViewState extends State .asMap() .entries .map((entry) { - return FeedbackFormField( + return EmojiFormField( question: entry.value, - error: _isFormFieldComplete[entry.key] - ? S + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S .of(context) - .warningYouNeedToSelectAtLeastOne - : null, + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: emojiSelected, ); }), ], @@ -362,13 +365,19 @@ class _ClassFeedbackViewState extends State ), const SizedBox(height: 24), ...homeworkQuestions.asMap().entries.map((entry) { - return FeedbackFormField( + return EmojiFormField( question: entry.value, - error: _isFormFieldComplete[entry.key] - ? S + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S .of(context) - .warningYouNeedToSelectAtLeastOne - : null, + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: emojiSelected, ); }), ], diff --git a/lib/pages/class_feedback/form_field.dart b/lib/pages/class_feedback/form_field.dart index 8504a64e1..6dfd30b2d 100644 --- a/lib/pages/class_feedback/form_field.dart +++ b/lib/pages/class_feedback/form_field.dart @@ -1,59 +1,59 @@ -import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; -import 'package:flutter/material.dart'; - -class FeedbackFormField extends StatelessWidget { - const FeedbackFormField( - {@required this.question, - this.error}); - - /// `question` you want to ask - final String question; - - /// `error` to be displayed below emojis row - /// - /// mostly used if no option is selected - final String error; - - /// Determines the number of radio buttons according to their taste - /// - /// 😠 😕 😐 ☺ 😍 - static final List _radioButtons = [1, 2, 3, 4, 5]; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - '$question', - style: const TextStyle( - fontSize: 18, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: _radioButtons.map((value) { - return RadioEmoji( - value: value, - ); - }).toList(), - ), - const SizedBox( - height: 2, - ), - Visibility( - visible: error != null, - child: Text( - '$error', - style: const TextStyle( - color: Colors.red, - ), - ), - ), - const SizedBox( - height: 10, - ), - ], - ); - } -} +// import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; +// import 'package:flutter/material.dart'; +// +// class FeedbackFormField extends StatelessWidget { +// const FeedbackFormField( +// {@required this.question, +// this.error}); +// +// /// `question` you want to ask +// final String question; +// +// /// `error` to be displayed below emojis row +// /// +// /// mostly used if no option is selected +// final String error; +// +// /// Determines the number of radio buttons according to their taste +// /// +// /// 😠 😕 😐 ☺ 😍 +// static final List _radioButtons = [1, 2, 3, 4, 5]; +// +// @override +// Widget build(BuildContext context) { +// return Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// Text( +// '$question', +// style: const TextStyle( +// fontSize: 18, +// ), +// ), +// Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: _radioButtons.map((value) { +// return RadioEmoji( +// value: value, +// ); +// }).toList(), +// ), +// const SizedBox( +// height: 2, +// ), +// Visibility( +// visible: error != null, +// child: Text( +// '$error', +// style: const TextStyle( +// color: Colors.red, +// ), +// ), +// ), +// const SizedBox( +// height: 10, +// ), +// ], +// ); +// } +// } diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 64c4c630e..3d6fe307b 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -1,6 +1,7 @@ +import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; -class RadioEmoji extends StatefulWidget { +/*class RadioEmoji extends StatefulWidget { const RadioEmoji({ @required this.value, }) : assert(value >= 1 && value <= 5); @@ -71,14 +72,12 @@ class _RadioEmojiState extends State }, ); } -} +}*/ class EmojiFormField extends FormField> { EmojiFormField({ @required Map initialValues, @required String question, - @required void Function() handleTap, - @required CurvedAnimation animation, String Function(Map) validator, Key key, }) : super( @@ -89,32 +88,55 @@ class EmojiFormField extends FormField> { builder: (state) { final context = state.context; final List emojis = [ - Icon( + const Icon( Icons.sentiment_very_dissatisfied, color: Colors.red, - size: animation.value * 10 + 20.0, ), - Icon( + const Icon( Icons.sentiment_dissatisfied, color: Colors.redAccent, - size: animation.value * 10 + 20.0, ), - Icon( + const Icon( Icons.sentiment_neutral, color: Colors.amber, - size: animation.value * 10 + 20.0, ), - Icon( + const Icon( Icons.sentiment_satisfied, color: Colors.lightGreen, - size: animation.value * 10 + 20.0, ), - Icon( + const Icon( Icons.sentiment_very_satisfied, color: Colors.green, - size: animation.value * 10 + 20.0, ) ]; + final List emojiControllers = + emojis.map((_) => SelectableController()).toList(); + final emojiSelectables = emojiControllers + .asMap() + .map( + (i, controller) => MapEntry( + i, + SelectableIcon( + icon: emojis[i], + controller: controller, + onSelected: (selected) { + print('Something was pressed $i $selected'); + if (selected) { + for (final c in emojiControllers) { + if (c != controller) { + c.deselect(); + } + } + controller.select(); + } else { + controller.deselect(); + } + }, + ), + ), + ) + .values + .toList(); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -124,56 +146,85 @@ class EmojiFormField extends FormField> { fontSize: 18, ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - alignment: Alignment.center, - height: 45, - child: ListView.builder( - shrinkWrap: true, - itemCount: emojis.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: handleTap, - child: Container( - decoration: BoxDecoration( - border: Border.all( - style: BorderStyle.none, width: 1), - shape: BoxShape.circle, - ), - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - child: emojis[index], - ), - ), - const SizedBox(width: 8), - ], - ); - }, - ), - ), - ), - if (state.hasError) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - state.errorText, - style: Theme.of(context) - .textTheme - .caption - .copyWith(color: Theme.of(context).errorColor), - ), - ), - ], + Container( + width: MediaQuery.of(context).size.width - 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: emojiSelectables, + ), ), + if (state.hasError) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + state.errorText, + style: Theme.of(context) + .textTheme + .caption + .copyWith(color: Theme.of(context).errorColor), + ), + ), ], ); }, ); } + +class SelectableIcon extends Selectable { + const SelectableIcon({ + Key key, + bool initiallySelected, + Function(bool) onSelected, + this.icon, + SelectableController controller, + }) : super( + initiallySelected: initiallySelected ?? false, + onSelected: onSelected, + controller: controller); + + final Icon icon; + + @override + _SelectableIconState createState() => _SelectableIconState(icon); +} + +class _SelectableIconState extends SelectableState { + _SelectableIconState(this.icon); + + Icon icon; + + @override + void initState() { + super.initState(); + isSelected = widget.initiallySelected; + } + + @override + Widget build(BuildContext context) { + widget.controller.selectableState = this; + + return GestureDetector( + onTap: () { + setState( + () { + isSelected = !isSelected; + widget.onSelected(isSelected); + }, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all(style: BorderStyle.none, width: 1), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: AnimatedContainer( + //color: isSelected ? Colors.green : Colors.transparent, + duration: const Duration(seconds: 1), + width: isSelected ? 100 : 15, + child: icon, + ), + ), + ); + } +} diff --git a/lib/widgets/selectable.dart b/lib/widgets/selectable.dart index f8462d5d7..f52f78446 100644 --- a/lib/widgets/selectable.dart +++ b/lib/widgets/selectable.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; class SelectableController { - _SelectableState _selectableState; + SelectableState selectableState; - bool get isSelected => _selectableState?._isSelected; + bool get isSelected => selectableState?.isSelected; void select() { - if (_selectableState == null) return; - if (!isSelected) _selectableState.isSelected = true; + if (selectableState == null) return; + if (!isSelected) selectableState.isSelected = true; } void deselect() { - if (_selectableState == null) return; - if (isSelected) _selectableState.isSelected = false; + if (selectableState == null) return; + if (isSelected) selectableState.isSelected = false; } } @@ -31,10 +31,10 @@ class Selectable extends StatefulWidget { final bool disabled; @override - _SelectableState createState() => _SelectableState(); + SelectableState createState() => SelectableState(); } -class _SelectableState extends State { +class SelectableState extends State { bool _isSelected; set isSelected(bool newValue) { @@ -52,7 +52,7 @@ class _SelectableState extends State { @override Widget build(BuildContext context) { - widget.controller?._selectableState = this; + widget.controller?.selectableState = this; return Container( decoration: BoxDecoration( From e9578b453d098b2e8b43bb17798d51dd479b4e4a Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 17 Apr 2021 22:55:54 +0300 Subject: [PATCH 07/59] Clean unused code --- .../class_feedback/class_feedback_view.dart | 33 +------ lib/pages/class_feedback/form_field.dart | 59 ------------ lib/widgets/radio_emoji.dart | 90 +++---------------- 3 files changed, 15 insertions(+), 167 deletions(-) delete mode 100644 lib/pages/class_feedback/form_field.dart diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 87f6a6c19..867e029bc 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -8,8 +8,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'form_field.dart'; - class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -19,20 +17,14 @@ class ClassFeedbackView extends StatefulWidget { _ClassFeedbackViewState createState() => _ClassFeedbackViewState(); } -class _ClassFeedbackViewState extends State - with TickerProviderStateMixin { +class _ClassFeedbackViewState extends State { final formKey = GlobalKey(); TextEditingController classController; bool agreedToResponsibilities = false; - final List _feedbackValue = []; - final List _isFormFieldComplete = []; List involvementPercentages = []; String selectedInvolvement; - AnimationController animationController; - CurvedAnimation animation; - Map emojiSelected = { 0: false, 1: false, @@ -53,16 +45,6 @@ class _ClassFeedbackViewState extends State '60% ... 80%', '80% ... 100%' ]; - - animationController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - - animation = CurvedAnimation( - parent: animationController, - curve: Curves.elasticOut, - ); } @override @@ -96,11 +78,6 @@ class _ClassFeedbackViewState extends State S.of(context).feedbackHomeworkQuestion3 ]; - for (int i = 0; i < lectureQuestions.length; ++i) { - _feedbackValue.add(-1); - _isFormFieldComplete.add(false); - } - return AppScaffold( title: Text(S.of(context).navigationClassFeedback), actions: [_submitButton()], @@ -115,8 +92,9 @@ class _ClassFeedbackViewState extends State children: [ Column(children: [ IconText( - icon: Icons.info_outline, - text: S.of(context).infoFormAnonymous), + icon: Icons.info_outline, + text: S.of(context).infoFormAnonymous, + ), TextFormField( enabled: false, controller: classController, @@ -284,7 +262,6 @@ class _ClassFeedbackViewState extends State S.of(context).uniEventTypeLecture, style: Theme.of(context).textTheme.headline6, ), - const SizedBox(height: 24), ...lectureQuestions.asMap().entries.map((entry) { return EmojiFormField( question: entry.value, @@ -315,7 +292,6 @@ class _ClassFeedbackViewState extends State S.of(context).sectionApplications, style: Theme.of(context).textTheme.headline6, ), - const SizedBox(height: 24), ...applicationsQuestions .asMap() .entries @@ -363,7 +339,6 @@ class _ClassFeedbackViewState extends State ), onChanged: (_) => setState(() {}), ), - const SizedBox(height: 24), ...homeworkQuestions.asMap().entries.map((entry) { return EmojiFormField( question: entry.value, diff --git a/lib/pages/class_feedback/form_field.dart b/lib/pages/class_feedback/form_field.dart deleted file mode 100644 index 6dfd30b2d..000000000 --- a/lib/pages/class_feedback/form_field.dart +++ /dev/null @@ -1,59 +0,0 @@ -// import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; -// import 'package:flutter/material.dart'; -// -// class FeedbackFormField extends StatelessWidget { -// const FeedbackFormField( -// {@required this.question, -// this.error}); -// -// /// `question` you want to ask -// final String question; -// -// /// `error` to be displayed below emojis row -// /// -// /// mostly used if no option is selected -// final String error; -// -// /// Determines the number of radio buttons according to their taste -// /// -// /// 😠 😕 😐 ☺ 😍 -// static final List _radioButtons = [1, 2, 3, 4, 5]; -// -// @override -// Widget build(BuildContext context) { -// return Column( -// crossAxisAlignment: CrossAxisAlignment.stretch, -// children: [ -// Text( -// '$question', -// style: const TextStyle( -// fontSize: 18, -// ), -// ), -// Row( -// mainAxisAlignment: MainAxisAlignment.center, -// children: _radioButtons.map((value) { -// return RadioEmoji( -// value: value, -// ); -// }).toList(), -// ), -// const SizedBox( -// height: 2, -// ), -// Visibility( -// visible: error != null, -// child: Text( -// '$error', -// style: const TextStyle( -// color: Colors.red, -// ), -// ), -// ), -// const SizedBox( -// height: 10, -// ), -// ], -// ); -// } -// } diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 3d6fe307b..0777c3191 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -1,79 +1,6 @@ import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; -/*class RadioEmoji extends StatefulWidget { - const RadioEmoji({ - @required this.value, - }) : assert(value >= 1 && value <= 5); - - /// Rating value between 1 and 5 - final int value; - - /// Emojis describing feedback status - static final List emojiIndex = ['😠', '😕', '😐', '☺', '😍']; - - @override - _RadioEmojiState createState() => _RadioEmojiState(); -} - -class _RadioEmojiState extends State - with SingleTickerProviderStateMixin { - AnimationController controller; - CurvedAnimation animation; - - String emoji; - - @override - void initState() { - super.initState(); - - emoji = RadioEmoji.emojiIndex[widget.value - 1]; - - controller = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - - animation = CurvedAnimation( - parent: controller, - curve: Curves.elasticOut, - ); - - controller.addListener(() => setState(() {})); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - child: Container( - decoration: BoxDecoration( - border: Border.all(style: BorderStyle.none, width: 1), - shape: BoxShape.circle, - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Container( - height: 35, - child: Text( - emoji, - style: TextStyle( - fontSize: animation.value * 10 + 20.0, - ), - ), - ), - ), - onTap: () { - controller.forward(); - }, - ); - } -}*/ - class EmojiFormField extends FormField> { EmojiFormField({ @required Map initialValues, @@ -91,26 +18,32 @@ class EmojiFormField extends FormField> { const Icon( Icons.sentiment_very_dissatisfied, color: Colors.red, + size: 30, ), const Icon( Icons.sentiment_dissatisfied, color: Colors.redAccent, + size: 30, ), const Icon( Icons.sentiment_neutral, color: Colors.amber, + size: 30, ), const Icon( Icons.sentiment_satisfied, color: Colors.lightGreen, + size: 30, ), const Icon( Icons.sentiment_very_satisfied, color: Colors.green, + size: 30, ) ]; final List emojiControllers = emojis.map((_) => SelectableController()).toList(); + final emojiSelectables = emojiControllers .asMap() .map( @@ -120,7 +53,6 @@ class EmojiFormField extends FormField> { icon: emojis[i], controller: controller, onSelected: (selected) { - print('Something was pressed $i $selected'); if (selected) { for (final c in emojiControllers) { if (c != controller) { @@ -140,6 +72,7 @@ class EmojiFormField extends FormField> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const SizedBox(height: 24), Text( '$question', style: const TextStyle( @@ -172,9 +105,8 @@ class EmojiFormField extends FormField> { class SelectableIcon extends Selectable { const SelectableIcon({ - Key key, bool initiallySelected, - Function(bool) onSelected, + void Function(bool) onSelected, this.icon, SelectableController controller, }) : super( @@ -219,9 +151,9 @@ class _SelectableIconState extends SelectableState { ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: AnimatedContainer( - //color: isSelected ? Colors.green : Colors.transparent, - duration: const Duration(seconds: 1), - width: isSelected ? 100 : 15, + height: isSelected ? 40 : 10, + width: isSelected ? 70 : 30, + duration: const Duration(milliseconds: 500), child: icon, ), ), From 1613f27aebcf5a4739da9fbe159ede191c2b2e54 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 18 Apr 2021 15:10:17 +0300 Subject: [PATCH 08/59] Replace class icon --- lib/pages/class_feedback/class_feedback_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 867e029bc..1b61a1f8e 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -5,6 +5,7 @@ import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; @@ -100,7 +101,7 @@ class _ClassFeedbackViewState extends State { controller: classController, decoration: InputDecoration( labelText: S.of(context).labelClass, - prefixIcon: const Icon(Icons.class__outlined), + prefixIcon: const Icon(FeatherIcons.bookOpen), ), onChanged: (_) => setState(() {}), ), From 8712a727ac935caec6a98e32ac09758db9587555 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 25 Apr 2021 19:06:25 +0300 Subject: [PATCH 09/59] Extract questions from Firestore --- lib/main.dart | 3 + .../class_feedback/class_feedback_view.dart | 220 +++++++++--------- .../class_feedback/feedback_provider.dart | 41 ++++ lib/widgets/radio_emoji.dart | 1 + 4 files changed, 153 insertions(+), 112 deletions(-) create mode 100644 lib/pages/class_feedback/feedback_provider.dart diff --git a/lib/main.dart b/lib/main.dart index 2b54677c1..c3064d2cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:acs_upb_mobile/authentication/view/sign_up_view.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/navigation/bottom_navigation_bar.dart'; import 'package:acs_upb_mobile/navigation/routes.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; @@ -47,12 +48,14 @@ Future main() async { final authProvider = AuthProvider(); final classProvider = ClassProvider(); final personProvider = PersonProvider(); + final feedbackProvider = FeedbackProvider(); runApp(MultiProvider(providers: [ ChangeNotifierProvider(create: (_) => authProvider), ChangeNotifierProvider(create: (_) => WebsiteProvider()), Provider(create: (_) => RequestProvider()), ChangeNotifierProvider(create: (_) => classProvider), + ChangeNotifierProvider(create: (_) => feedbackProvider), ChangeNotifierProvider(create: (_) => personProvider), ChangeNotifierProvider(create: (_) => QuestionProvider()), ChangeNotifierProvider(create: (_) => NewsProvider()), diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 1b61a1f8e..4a00a2a8a 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -1,5 +1,8 @@ +import 'package:acs_upb_mobile/pages/class_feedback/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +import 'package:acs_upb_mobile/widgets/autocomplete.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; @@ -8,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:recase/recase.dart'; class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -25,6 +29,9 @@ class _ClassFeedbackViewState extends State { List involvementPercentages = []; String selectedInvolvement; + Person selectedAssistant; + List classTeachers = []; + List feedbackQuestions = []; Map emojiSelected = { 0: false, @@ -46,38 +53,89 @@ class _ClassFeedbackViewState extends State { '60% ... 80%', '80% ... 100%' ]; + Provider.of(context, listen: false) + .fetchPeople(context: context) + .then((teachers) => setState(() => classTeachers = teachers)); + Provider.of(context, listen: false) + .fetchQuestions(context: context) + .then((questions) => setState(() => feedbackQuestions = questions)); + } + + Widget autocompleteAssistant(BuildContext context) { + return Autocomplete( + key: const Key('AutocompleteAssistant'), + fieldViewBuilder: (BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted) { + textEditingController.text = selectedAssistant?.name; + return TextFormField( + controller: textEditingController, + decoration: InputDecoration( + labelText: S.of(context).labelAssistant, + prefixIcon: const Icon(FeatherIcons.user), + ), + focusNode: focusNode, + onFieldSubmitted: (String value) { + onFieldSubmitted(); + }, + ); + }, + displayStringForOption: (Person person) => person.name, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '' || textEditingValue.text.isEmpty) { + return const Iterable.empty(); + } + if (classTeachers.where((Person person) { + return person.name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }).isEmpty) { + final List inputTeachers = []; + final Person inputTeacher = + Person(name: textEditingValue.text.titleCase); + inputTeachers.add(inputTeacher); + return inputTeachers; + } + + return classTeachers.where((Person person) { + return person.name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }); + }, + onSelected: (Person selection) { + formKey.currentState.validate(); + setState(() { + selectedAssistant = selection; + }); + }, + ); } @override Widget build(BuildContext context) { final personProvider = Provider.of(context); + final feedbackProvider = Provider.of(context); + print(feedbackQuestions.length); + final List generalQuestions = + feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'general'); + //final List involvementQuestions = feedbackProvider + // .getQuestionsByCategory(feedbackQuestions, 'involvement'); + //final String involvementQuestion = involvementQuestions?.single; + //print(involvementQuestion); - final generalQuestions = [ - S.of(context).feedbackGeneralQuestion1, - S.of(context).feedbackGeneralQuestion3, - S.of(context).feedbackGeneralQuestion4 - ]; + final List lectureQuestions = + feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'lecture'); - final lectureQuestions = [ - S.of(context).feedbackLectureQuestion1, - S.of(context).feedbackLectureApplicationsQuestion2, - S.of(context).feedbackLectureQuestion3, - S.of(context).feedbackLectureQuestion4, - S.of(context).feedbackLectureQuestion5 - ]; + final List applicationsQuestions = feedbackProvider + .getQuestionsByCategory(feedbackQuestions, 'applications'); - final applicationsQuestions = [ - S.of(context).feedbackApplicationsQuestion1, - S.of(context).feedbackLectureApplicationsQuestion2, - S.of(context).feedbackApplicationsQuestion2, - S.of(context).feedbackApplicationsQuestion3, - S.of(context).feedbackApplicationsQuestion4, - ]; + final List homeworkQuestions = + feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'homework'); - final homeworkQuestions = [ - S.of(context).feedbackHomeworkQuestion2, - S.of(context).feedbackHomeworkQuestion3 - ]; + final List personalQuestions = + feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'personal'); return AppScaffold( title: Text(S.of(context).navigationClassFeedback), @@ -127,13 +185,7 @@ class _ClassFeedbackViewState extends State { } }, ), - TextFormField( - decoration: InputDecoration( - labelText: S.of(context).labelAssistant, - prefixIcon: const Icon(Icons.person_outline), - ), - onChanged: (_) => setState(() {}), - ), + autocompleteAssistant(context), Padding( padding: const EdgeInsets.all(10), child: Row( @@ -371,90 +423,34 @@ class _ClassFeedbackViewState extends State { style: Theme.of(context).textTheme.headline6, ), const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion1, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], - ), - ), - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion2, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, + ...personalQuestions.asMap().entries.map((entry) { + return Column( + children: [ + Text( + entry.value, + style: const TextStyle( + fontSize: 18, ), - ], - ), - ), - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion3, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + keyboardType: + TextInputType.multiline, + maxLines: null, + ), + ], + ), ), - ], - ), - ), - ), - const SizedBox(height: 24), - Text( - S.of(context).feedbackPersonalQuestion4, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], - ), - ), - ), - const SizedBox(height: 24), + ), + const SizedBox(height: 24), + ], + ); + }), ], ), ), diff --git a/lib/pages/class_feedback/feedback_provider.dart b/lib/pages/class_feedback/feedback_provider.dart new file mode 100644 index 000000000..bd32acf0e --- /dev/null +++ b/lib/pages/class_feedback/feedback_provider.dart @@ -0,0 +1,41 @@ +import 'package:acs_upb_mobile/resources/locale_provider.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:acs_upb_mobile/widgets/toast.dart'; +import 'package:acs_upb_mobile/generated/l10n.dart'; + +class FeedbackProvider with ChangeNotifier { + Future> fetchQuestions({BuildContext context}) async { + try { + final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance + .collection('forms') + .doc('class_feedback_questions') + .get(); + final Map data = documentSnapshot['questions']; + final List values = data.values.toList(); + return values; + } catch (e) { + print(e); + if (context != null) { + AppToast.show(S.of(context).errorSomethingWentWrong); + } + return null; + } + } + + List getQuestionsByCategory( + List questions, String category) { + final List filteredQuestions = []; + final List filterQuestions = questions + .where((element) => + element is Map && element['category'] == category) + .toList(); + for (final Map element in filterQuestions) { + final List qs = element.values.toList(); + filteredQuestions.add( + qs[qs.indexWhere((element) => element is Map)] + [LocaleProvider.localeString]); + } + return filteredQuestions; + } +} diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 0777c3191..1463a2d41 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -86,6 +86,7 @@ class EmojiFormField extends FormField> { children: emojiSelectables, ), ), + const SizedBox(height: 12), if (state.hasError) Padding( padding: const EdgeInsets.only(top: 8), From 2529b477f1afacd558e300e00dcf565faabc3dd6 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 25 Apr 2021 19:22:08 +0300 Subject: [PATCH 10/59] Remove context from provider --- lib/pages/class_feedback/class_feedback_view.dart | 4 ++-- lib/pages/class_feedback/feedback_provider.dart | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/class_feedback_view.dart index 4a00a2a8a..3c4a877b7 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/class_feedback_view.dart @@ -54,10 +54,10 @@ class _ClassFeedbackViewState extends State { '80% ... 100%' ]; Provider.of(context, listen: false) - .fetchPeople(context: context) + .fetchPeople() .then((teachers) => setState(() => classTeachers = teachers)); Provider.of(context, listen: false) - .fetchQuestions(context: context) + .fetchQuestions() .then((questions) => setState(() => feedbackQuestions = questions)); } diff --git a/lib/pages/class_feedback/feedback_provider.dart b/lib/pages/class_feedback/feedback_provider.dart index bd32acf0e..6feecc578 100644 --- a/lib/pages/class_feedback/feedback_provider.dart +++ b/lib/pages/class_feedback/feedback_provider.dart @@ -5,7 +5,7 @@ import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; class FeedbackProvider with ChangeNotifier { - Future> fetchQuestions({BuildContext context}) async { + Future> fetchQuestions() async { try { final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance .collection('forms') @@ -16,9 +16,7 @@ class FeedbackProvider with ChangeNotifier { return values; } catch (e) { print(e); - if (context != null) { - AppToast.show(S.of(context).errorSomethingWentWrong); - } + AppToast.show(S.current.errorSomethingWentWrong); return null; } } From a055bbb34e75213770c5e4be45345e4738d45e9b Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 25 Apr 2021 23:50:24 +0300 Subject: [PATCH 11/59] Add question type --- lib/main.dart | 2 +- .../class_feedback/feedback_provider.dart | 39 ---------- .../model/class_feedback_answer.dart | 17 +++++ .../service/feedback_provider.dart | 72 +++++++++++++++++++ .../{ => view}/class_feedback_view.dart | 66 +++++++++++------ lib/pages/classes/view/class_view.dart | 2 +- 6 files changed, 136 insertions(+), 62 deletions(-) delete mode 100644 lib/pages/class_feedback/feedback_provider.dart create mode 100644 lib/pages/class_feedback/model/class_feedback_answer.dart create mode 100644 lib/pages/class_feedback/service/feedback_provider.dart rename lib/pages/class_feedback/{ => view}/class_feedback_view.dart (89%) diff --git a/lib/main.dart b/lib/main.dart index 899b6910c..1a5abbb19 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:acs_upb_mobile/authentication/view/sign_up_view.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/navigation/bottom_navigation_bar.dart'; import 'package:acs_upb_mobile/navigation/routes.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; diff --git a/lib/pages/class_feedback/feedback_provider.dart b/lib/pages/class_feedback/feedback_provider.dart deleted file mode 100644 index 6feecc578..000000000 --- a/lib/pages/class_feedback/feedback_provider.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:acs_upb_mobile/widgets/toast.dart'; -import 'package:acs_upb_mobile/generated/l10n.dart'; - -class FeedbackProvider with ChangeNotifier { - Future> fetchQuestions() async { - try { - final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance - .collection('forms') - .doc('class_feedback_questions') - .get(); - final Map data = documentSnapshot['questions']; - final List values = data.values.toList(); - return values; - } catch (e) { - print(e); - AppToast.show(S.current.errorSomethingWentWrong); - return null; - } - } - - List getQuestionsByCategory( - List questions, String category) { - final List filteredQuestions = []; - final List filterQuestions = questions - .where((element) => - element is Map && element['category'] == category) - .toList(); - for (final Map element in filterQuestions) { - final List qs = element.values.toList(); - filteredQuestions.add( - qs[qs.indexWhere((element) => element is Map)] - [LocaleProvider.localeString]); - } - return filteredQuestions; - } -} diff --git a/lib/pages/class_feedback/model/class_feedback_answer.dart b/lib/pages/class_feedback/model/class_feedback_answer.dart new file mode 100644 index 000000000..cf13db463 --- /dev/null +++ b/lib/pages/class_feedback/model/class_feedback_answer.dart @@ -0,0 +1,17 @@ +import 'package:acs_upb_mobile/pages/people/model/person.dart'; + +class ClassFeedbackQuestionAnswer { + ClassFeedbackQuestionAnswer({ + this.questionTextAnswer, + this.questionNumericAnswer, + this.teacher, + this.assistant, + this.questionNumber, + }); + + final String questionTextAnswer; + final int questionNumericAnswer; + final Person teacher; + final Person assistant; + final String questionNumber; +} diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart new file mode 100644 index 000000000..6ea6d9ad1 --- /dev/null +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -0,0 +1,72 @@ +import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; +import 'package:acs_upb_mobile/resources/locale_provider.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:acs_upb_mobile/widgets/toast.dart'; +import 'package:acs_upb_mobile/generated/l10n.dart'; + +extension ClassFeedbackQuestionAnswerExtension on ClassFeedbackQuestionAnswer { + Map toData() { + final Map data = {}; + + if (questionTextAnswer != null) data['answer'] = questionTextAnswer; + if (questionNumericAnswer != null) data['rating'] = questionNumericAnswer; + data['dateSubmitted'] = Timestamp.now(); + data['teacher'] = teacher; + data['assistant'] = assistant; + + return data; + } +} + +class FeedbackProvider with ChangeNotifier { + Future addResponse(ClassFeedbackQuestionAnswer response) async { + try { + await FirebaseFirestore.instance + .collection('forms') + .doc('class_feedback_answers') + .collection(response.questionNumber) + .add(response.toData()); + notifyListeners(); + return true; + } catch (e) { + print(e); + AppToast.show(S.current.errorSomethingWentWrong); + return false; + } + } + + Future> fetchQuestions() async { + try { + final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance + .collection('forms') + .doc('class_feedback_questions') + .get(); + final Map data = documentSnapshot['questions']; + final List values = data.values.toList(); + return values; + } catch (e) { + print(e); + AppToast.show(S.current.errorSomethingWentWrong); + return null; + } + } + + List getQuestionsByCategoryAndType( + List questions, String category, String type) { + final List filteredQuestions = []; + final List filterQuestions = questions + .where((element) => + element is Map && + element['category'] == category && + element['type'] == type) + .toList(); + for (final Map element in filterQuestions) { + final List qs = element.values.toList(); + filteredQuestions.add( + qs[qs.indexWhere((element) => element is Map)] + [LocaleProvider.localeString]); + } + return filteredQuestions; + } +} diff --git a/lib/pages/class_feedback/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart similarity index 89% rename from lib/pages/class_feedback/class_feedback_view.dart rename to lib/pages/class_feedback/view/class_feedback_view.dart index 3c4a877b7..825e68e50 100644 --- a/lib/pages/class_feedback/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -1,4 +1,5 @@ -import 'package:acs_upb_mobile/pages/class_feedback/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; @@ -6,6 +7,7 @@ import 'package:acs_upb_mobile/widgets/autocomplete.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; +import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; @@ -117,25 +119,32 @@ class _ClassFeedbackViewState extends State { Widget build(BuildContext context) { final personProvider = Provider.of(context); final feedbackProvider = Provider.of(context); - print(feedbackQuestions.length); - final List generalQuestions = - feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'general'); - //final List involvementQuestions = feedbackProvider - // .getQuestionsByCategory(feedbackQuestions, 'involvement'); - //final String involvementQuestion = involvementQuestions?.single; - //print(involvementQuestion); - final List lectureQuestions = - feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'lecture'); + final List generalQuestionsRating = feedbackProvider + .getQuestionsByCategoryAndType(feedbackQuestions, 'general', 'rating'); - final List applicationsQuestions = feedbackProvider - .getQuestionsByCategory(feedbackQuestions, 'applications'); + final List generalQuestionsInput = feedbackProvider + .getQuestionsByCategoryAndType(feedbackQuestions, 'general', 'input'); - final List homeworkQuestions = - feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'homework'); + final List involvementQuestions = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions, 'involvement', 'input'); - final List personalQuestions = - feedbackProvider.getQuestionsByCategory(feedbackQuestions, 'personal'); + final List lectureQuestions = feedbackProvider + .getQuestionsByCategoryAndType(feedbackQuestions, 'lecture', 'rating'); + + final List applicationsQuestions = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions, 'applications', 'rating'); + + final List homeworkQuestionsRating = feedbackProvider + .getQuestionsByCategoryAndType(feedbackQuestions, 'homework', 'rating'); + + final List homeworkQuestionsInput = feedbackProvider + .getQuestionsByCategoryAndType(feedbackQuestions, 'homework', 'input'); + + final List personalQuestions = feedbackProvider + .getQuestionsByCategoryAndType(feedbackQuestions, 'personal', 'input'); return AppScaffold( title: Text(S.of(context).navigationClassFeedback), @@ -221,7 +230,7 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - S.of(context).feedbackGeneralQuestion2, + generalQuestionsInput.single, style: const TextStyle( fontSize: 18, ), @@ -234,7 +243,7 @@ class _ClassFeedbackViewState extends State { onChanged: (_) => setState(() {}), ), const SizedBox(height: 24), - ...generalQuestions.asMap().entries.map( + ...generalQuestionsRating.asMap().entries.map( (entry) { return EmojiFormField( question: entry.value, @@ -268,7 +277,7 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - S.of(context).feedbackActivitiesQuestion, + involvementQuestions.single, style: const TextStyle( fontSize: 18, ), @@ -380,7 +389,7 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - S.of(context).feedbackHomeworkQuestion1, + homeworkQuestionsInput.single, style: const TextStyle( fontSize: 18, ), @@ -392,7 +401,10 @@ class _ClassFeedbackViewState extends State { ), onChanged: (_) => setState(() {}), ), - ...homeworkQuestions.asMap().entries.map((entry) { + ...homeworkQuestionsRating + .asMap() + .entries + .map((entry) { return EmojiFormField( question: entry.value, validator: (selection) { @@ -469,6 +481,18 @@ class _ClassFeedbackViewState extends State { text: 'Submit', onPressed: () async { if (!formKey.currentState.validate()) return; + + final response = ClassFeedbackQuestionAnswer( + + ); + + final res = + await Provider.of(context, listen: false) + .addResponse(response); + if (res) { + Navigator.of(context).pop(); + AppToast.show(S.current.messageEventAdded); + } }, ); } diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 0ef956458..09e4b241f 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -1,6 +1,6 @@ import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/class_feedback_view.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; From 67729073ddf2742360faaac2d879bda9b6a5d8e9 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 5 May 2021 23:56:26 +0300 Subject: [PATCH 12/59] Begin saving questions' answers --- .../model/class_feedback_answer.dart | 8 +-- .../service/feedback_provider.dart | 7 +-- .../view/class_feedback_view.dart | 53 +++++++++++++++++-- lib/widgets/radio_emoji.dart | 20 +++++++ 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/lib/pages/class_feedback/model/class_feedback_answer.dart b/lib/pages/class_feedback/model/class_feedback_answer.dart index cf13db463..92b21f263 100644 --- a/lib/pages/class_feedback/model/class_feedback_answer.dart +++ b/lib/pages/class_feedback/model/class_feedback_answer.dart @@ -4,14 +4,16 @@ class ClassFeedbackQuestionAnswer { ClassFeedbackQuestionAnswer({ this.questionTextAnswer, this.questionNumericAnswer, - this.teacher, + this.className, + this.teacherName, this.assistant, this.questionNumber, }); final String questionTextAnswer; - final int questionNumericAnswer; - final Person teacher; + final String questionNumericAnswer; + final String className; + final String teacherName; final Person assistant; final String questionNumber; } diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 6ea6d9ad1..290a28a6d 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -12,8 +12,9 @@ extension ClassFeedbackQuestionAnswerExtension on ClassFeedbackQuestionAnswer { if (questionTextAnswer != null) data['answer'] = questionTextAnswer; if (questionNumericAnswer != null) data['rating'] = questionNumericAnswer; data['dateSubmitted'] = Timestamp.now(); - data['teacher'] = teacher; - data['assistant'] = assistant; + data['class'] = className; + data['teacher'] = teacherName; + data['assistant'] = assistant.name; return data; } @@ -27,7 +28,7 @@ class FeedbackProvider with ChangeNotifier { .doc('class_feedback_answers') .collection(response.questionNumber) .add(response.toData()); - notifyListeners(); + //notifyListeners(); return true; } catch (e) { print(e); diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 825e68e50..c41f1f19a 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -31,9 +31,12 @@ class _ClassFeedbackViewState extends State { List involvementPercentages = []; String selectedInvolvement; + String selectedTeacherName; Person selectedAssistant; List classTeachers = []; List feedbackQuestions = []; + Map> responses = {}; + TextEditingController gradeController; Map emojiSelected = { 0: false, @@ -48,6 +51,7 @@ class _ClassFeedbackViewState extends State { super.initState(); classController = TextEditingController(text: widget.classHeader?.id ?? ''); + gradeController = TextEditingController(); involvementPercentages = [ '0% ... 20%', '20% ... 40%', @@ -81,6 +85,12 @@ class _ClassFeedbackViewState extends State { onFieldSubmitted: (String value) { onFieldSubmitted(); }, + validator: (_) { + if (textEditingController.text.isEmpty ?? true) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, ); }, displayStringForOption: (Person person) => person.name, @@ -178,6 +188,7 @@ class _ClassFeedbackViewState extends State { builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { final lecturerName = snapshot.data; + selectedTeacherName = lecturerName; return TextFormField( enabled: false, controller: TextEditingController( @@ -230,23 +241,39 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - generalQuestionsInput.single, + //generalQuestionsInput.single, + S.current.feedbackGeneralQuestion2, style: const TextStyle( fontSize: 18, ), ), TextFormField( + controller: gradeController, decoration: InputDecoration( labelText: S.of(context).labelGrade, prefixIcon: const Icon(Icons.grade_outlined), ), + validator: (value) { + if (value?.isEmpty ?? true) { + return S + .current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, onChanged: (_) => setState(() {}), ), const SizedBox(height: 24), ...generalQuestionsRating.asMap().entries.map( (entry) { + final length = generalQuestionsRating.length; return EmojiFormField( question: entry.value, + questionIndex: entry.key, + responses: responses, + onSaved: (value) { + //responses = responses; + //print(responses); + }, validator: (selection) { if (selection.values .where((e) => e != false) @@ -277,7 +304,8 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - involvementQuestions.single, + //involvementQuestions.single, + S.current.feedbackActivitiesQuestion, style: const TextStyle( fontSize: 18, ), @@ -389,7 +417,8 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - homeworkQuestionsInput.single, + //homeworkQuestionsInput.single, + S.current.feedbackHomeworkQuestion1, style: const TextStyle( fontSize: 18, ), @@ -482,10 +511,20 @@ class _ClassFeedbackViewState extends State { onPressed: () async { if (!formKey.currentState.validate()) return; - final response = ClassFeedbackQuestionAnswer( + if (!agreedToResponsibilities) { + AppToast.show( + '${S.current.warningAgreeTo}${S.current.labelPermissionsConsent}.'); + return; + } + final response = ClassFeedbackQuestionAnswer( + assistant: selectedAssistant, + teacherName: selectedTeacherName, + className: classController.text, + questionNumber: '0', + questionNumericAnswer: gradeController.text.toString(), ); - + //print(responses); final res = await Provider.of(context, listen: false) .addResponse(response); @@ -493,6 +532,10 @@ class _ClassFeedbackViewState extends State { Navigator.of(context).pop(); AppToast.show(S.current.messageEventAdded); } + setState(() { + //print(responses); + formKey.currentState.save(); + }); }, ); } diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 1463a2d41..553a78a16 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -5,11 +5,15 @@ class EmojiFormField extends FormField> { EmojiFormField({ @required Map initialValues, @required String question, + FormFieldSetter> onSaved, String Function(Map) validator, + int questionIndex, + Map> responses, Key key, }) : super( key: key, validator: validator, + onSaved: onSaved, autovalidateMode: AutovalidateMode.onUserInteraction, initialValue: initialValues, builder: (state) { @@ -57,11 +61,27 @@ class EmojiFormField extends FormField> { for (final c in emojiControllers) { if (c != controller) { c.deselect(); + //TODO + state.value[i] = selected; + state.value[emojiControllers.indexOf(c)] = + !selected; } } controller.select(); + state.value[i] = selected; + state.didChange(state.value); + responses.addAll({questionIndex: null}); + responses.update( + questionIndex, (value) => state.value); + //print('2. ${state.value}'); } else { controller.deselect(); + state.value[i] = selected; + state.didChange(state.value); + responses.addAll({questionIndex: null}); + responses.update( + questionIndex, (value) => state.value); + //print('3. ${state.value}'); } }, ), From d4ab040169564fa5f6ce6450c9a2c2ea113dc9ca Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Fri, 7 May 2021 00:55:10 +0300 Subject: [PATCH 13/59] Improve answers' saving process --- lib/generated/intl/messages_en.dart | 21 -- lib/generated/intl/messages_ro.dart | 21 -- lib/generated/l10n.dart | 210 ------------------ lib/l10n/intl_en.arb | 22 -- lib/l10n/intl_ro.arb | 22 -- .../view/class_feedback_view.dart | 173 ++++++++++----- lib/widgets/radio_emoji.dart | 8 - 7 files changed, 116 insertions(+), 361 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 25664bc90..aa73d536e 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -102,27 +102,6 @@ class MessageLookup extends MessageLookupByLibrary { "errorPictureSizeToBig" : MessageLookupByLibrary.simpleMessage("Please select a picture that is less than 5MB."), "errorSomethingWentWrong" : MessageLookupByLibrary.simpleMessage("Something went wrong."), "errorTooManyRequests" : MessageLookupByLibrary.simpleMessage("There have been too many requests from this device."), - "feedbackActivitiesQuestion" : MessageLookupByLibrary.simpleMessage("Approximate number of activities that you attended (lectures + applications):"), - "feedbackApplicationsQuestion1" : MessageLookupByLibrary.simpleMessage("Does the assistant master the field of study well?"), - "feedbackApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Did the applications stimulate discussions and did the assistant clearly answer students\' questions?"), - "feedbackApplicationsQuestion3" : MessageLookupByLibrary.simpleMessage("Was the assistant\'s behaviour towards students appropriate?"), - "feedbackApplicationsQuestion4" : MessageLookupByLibrary.simpleMessage("Are the teaching materials provided sufficient to understand the applications?"), - "feedbackGeneralQuestion1" : MessageLookupByLibrary.simpleMessage("Is your overall assessment of this discipline positive?"), - "feedbackGeneralQuestion2" : MessageLookupByLibrary.simpleMessage("What grade do you expect to get in this class? (1-10)"), - "feedbackGeneralQuestion3" : MessageLookupByLibrary.simpleMessage("Is the overall load in this class lower than in other classes that offer the same number of credits?"), - "feedbackGeneralQuestion4" : MessageLookupByLibrary.simpleMessage("Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?"), - "feedbackHomeworkQuestion1" : MessageLookupByLibrary.simpleMessage("Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours)."), - "feedbackHomeworkQuestion2" : MessageLookupByLibrary.simpleMessage("Were the number and difficulty of the homework adequate?"), - "feedbackHomeworkQuestion3" : MessageLookupByLibrary.simpleMessage("Did the homework / projects / practical activities help to understand the class?"), - "feedbackLectureApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Was the exposure method appropriate?"), - "feedbackLectureQuestion1" : MessageLookupByLibrary.simpleMessage("Does the teacher master the field of study well?"), - "feedbackLectureQuestion3" : MessageLookupByLibrary.simpleMessage("Did the lecture stimulate discussions and did the teacher clearly answer students\' questions?"), - "feedbackLectureQuestion4" : MessageLookupByLibrary.simpleMessage("Was the teacher\'s behaviour towards students appropriate?"), - "feedbackLectureQuestion5" : MessageLookupByLibrary.simpleMessage("Are the teaching materials provided sufficient to understand the lecture?"), - "feedbackPersonalQuestion1" : MessageLookupByLibrary.simpleMessage("What are the positive aspects of this class?"), - "feedbackPersonalQuestion2" : MessageLookupByLibrary.simpleMessage("What do you think needs to be improved in this class?"), - "feedbackPersonalQuestion3" : MessageLookupByLibrary.simpleMessage("In your opinion, the main difficulty in pursuing this class comes from:"), - "feedbackPersonalQuestion4" : MessageLookupByLibrary.simpleMessage("Other personal comments or suggestions regarding the activities carried out in this discipline:"), "fileAcsBanner" : MessageLookupByLibrary.simpleMessage("assets/images/acs_banner_en.png"), "filterMenuRelevance" : MessageLookupByLibrary.simpleMessage("Filter by relevance"), "filterMenuShowAll" : MessageLookupByLibrary.simpleMessage("Show all"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 7b54c6ce8..7d449bc30 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -102,27 +102,6 @@ class MessageLookup extends MessageLookupByLibrary { "errorPictureSizeToBig" : MessageLookupByLibrary.simpleMessage("Selectați o fotografie care are mai puțin de 5MB."), "errorSomethingWentWrong" : MessageLookupByLibrary.simpleMessage("A apărut o problemă."), "errorTooManyRequests" : MessageLookupByLibrary.simpleMessage("Au fost trimise prea multe cereri de pe acest dispozitiv."), - "feedbackActivitiesQuestion" : MessageLookupByLibrary.simpleMessage("Numărul aproximativ de activități la care ați participat (curs + aplicații):"), - "feedbackApplicationsQuestion1" : MessageLookupByLibrary.simpleMessage("Cadrul didactic stăpânește bine domeniul de studiu?"), - "feedbackApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Aplicațiile au stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?"), - "feedbackApplicationsQuestion3" : MessageLookupByLibrary.simpleMessage("Comportamentul cadrului didactic față de studenți a fost adecvat?"), - "feedbackApplicationsQuestion4" : MessageLookupByLibrary.simpleMessage("Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea aplicațiilor?"), - "feedbackGeneralQuestion1" : MessageLookupByLibrary.simpleMessage("Evaluarea dumneavoastră generală cu privire la această disciplină este pozitivă?"), - "feedbackGeneralQuestion2" : MessageLookupByLibrary.simpleMessage("Care este nota pe care vă așteptați să o obțineți la această disciplină? (1-10)"), - "feedbackGeneralQuestion3" : MessageLookupByLibrary.simpleMessage("Încărcarea generală la această disciplină este mai mică decât cea a altor discipline care oferă același număr de credite?"), - "feedbackGeneralQuestion4" : MessageLookupByLibrary.simpleMessage("Dotarea (locație/echipamente hardware și software/suport digital) este adecvată activităților acestei discipline?"), - "feedbackHomeworkQuestion1" : MessageLookupByLibrary.simpleMessage("Estimați numărul mediu de ore pe săptămână dedicate rezolvării temelor (un număr între 1 și 10 ore)."), - "feedbackHomeworkQuestion2" : MessageLookupByLibrary.simpleMessage("Numărul și dificultatea temelor au fost adecvate?"), - "feedbackHomeworkQuestion3" : MessageLookupByLibrary.simpleMessage("Temele/proiectele/activitățile practice au ajutat la înțelegerea materiei?"), - "feedbackLectureApplicationsQuestion2" : MessageLookupByLibrary.simpleMessage("Metoda de expunere a fost potrivită?"), - "feedbackLectureQuestion1" : MessageLookupByLibrary.simpleMessage("Cadrul didactic stăpânește bine domeniul de studiu?"), - "feedbackLectureQuestion3" : MessageLookupByLibrary.simpleMessage("Cursul a stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?"), - "feedbackLectureQuestion4" : MessageLookupByLibrary.simpleMessage("Comportamentul cadrului didactic față de studenți a fost adecvat?"), - "feedbackLectureQuestion5" : MessageLookupByLibrary.simpleMessage("Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea cursului?"), - "feedbackPersonalQuestion1" : MessageLookupByLibrary.simpleMessage("Care sunt aspectele pozitive ale acestei discipline?"), - "feedbackPersonalQuestion2" : MessageLookupByLibrary.simpleMessage("Ce considerați că trebuie îmbunătățit la această disciplină?"), - "feedbackPersonalQuestion3" : MessageLookupByLibrary.simpleMessage("După părerea dumneavoastră, dificultatea principală în urmărirea acestei discipline provine din:"), - "feedbackPersonalQuestion4" : MessageLookupByLibrary.simpleMessage("Alte comentarii personale sau sugestii referitoare la activitățile desfășurate la această disciplină:"), "fileAcsBanner" : MessageLookupByLibrary.simpleMessage("assets/images/acs_banner_ro.png"), "filterMenuRelevance" : MessageLookupByLibrary.simpleMessage("Filtrează după relevanță"), "filterMenuShowAll" : MessageLookupByLibrary.simpleMessage("Arată tot"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 10d6b49a8..aef127e96 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2725,216 +2725,6 @@ class S { ); } - /// `Is your overall assessment of this discipline positive?` - String get feedbackGeneralQuestion1 { - return Intl.message( - 'Is your overall assessment of this discipline positive?', - name: 'feedbackGeneralQuestion1', - desc: '', - args: [], - ); - } - - /// `What grade do you expect to get in this class? (1-10)` - String get feedbackGeneralQuestion2 { - return Intl.message( - 'What grade do you expect to get in this class? (1-10)', - name: 'feedbackGeneralQuestion2', - desc: '', - args: [], - ); - } - - /// `Is the overall load in this class lower than in other classes that offer the same number of credits?` - String get feedbackGeneralQuestion3 { - return Intl.message( - 'Is the overall load in this class lower than in other classes that offer the same number of credits?', - name: 'feedbackGeneralQuestion3', - desc: '', - args: [], - ); - } - - /// `Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?` - String get feedbackGeneralQuestion4 { - return Intl.message( - 'Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?', - name: 'feedbackGeneralQuestion4', - desc: '', - args: [], - ); - } - - /// `Approximate number of activities that you attended (lectures + applications):` - String get feedbackActivitiesQuestion { - return Intl.message( - 'Approximate number of activities that you attended (lectures + applications):', - name: 'feedbackActivitiesQuestion', - desc: '', - args: [], - ); - } - - /// `Does the teacher master the field of study well?` - String get feedbackLectureQuestion1 { - return Intl.message( - 'Does the teacher master the field of study well?', - name: 'feedbackLectureQuestion1', - desc: '', - args: [], - ); - } - - /// `Was the exposure method appropriate?` - String get feedbackLectureApplicationsQuestion2 { - return Intl.message( - 'Was the exposure method appropriate?', - name: 'feedbackLectureApplicationsQuestion2', - desc: '', - args: [], - ); - } - - /// `Did the lecture stimulate discussions and did the teacher clearly answer students' questions?` - String get feedbackLectureQuestion3 { - return Intl.message( - 'Did the lecture stimulate discussions and did the teacher clearly answer students\' questions?', - name: 'feedbackLectureQuestion3', - desc: '', - args: [], - ); - } - - /// `Was the teacher's behaviour towards students appropriate?` - String get feedbackLectureQuestion4 { - return Intl.message( - 'Was the teacher\'s behaviour towards students appropriate?', - name: 'feedbackLectureQuestion4', - desc: '', - args: [], - ); - } - - /// `Are the teaching materials provided sufficient to understand the lecture?` - String get feedbackLectureQuestion5 { - return Intl.message( - 'Are the teaching materials provided sufficient to understand the lecture?', - name: 'feedbackLectureQuestion5', - desc: '', - args: [], - ); - } - - /// `Does the assistant master the field of study well?` - String get feedbackApplicationsQuestion1 { - return Intl.message( - 'Does the assistant master the field of study well?', - name: 'feedbackApplicationsQuestion1', - desc: '', - args: [], - ); - } - - /// `Did the applications stimulate discussions and did the assistant clearly answer students' questions?` - String get feedbackApplicationsQuestion2 { - return Intl.message( - 'Did the applications stimulate discussions and did the assistant clearly answer students\' questions?', - name: 'feedbackApplicationsQuestion2', - desc: '', - args: [], - ); - } - - /// `Was the assistant's behaviour towards students appropriate?` - String get feedbackApplicationsQuestion3 { - return Intl.message( - 'Was the assistant\'s behaviour towards students appropriate?', - name: 'feedbackApplicationsQuestion3', - desc: '', - args: [], - ); - } - - /// `Are the teaching materials provided sufficient to understand the applications?` - String get feedbackApplicationsQuestion4 { - return Intl.message( - 'Are the teaching materials provided sufficient to understand the applications?', - name: 'feedbackApplicationsQuestion4', - desc: '', - args: [], - ); - } - - /// `Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours).` - String get feedbackHomeworkQuestion1 { - return Intl.message( - 'Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours).', - name: 'feedbackHomeworkQuestion1', - desc: '', - args: [], - ); - } - - /// `Were the number and difficulty of the homework adequate?` - String get feedbackHomeworkQuestion2 { - return Intl.message( - 'Were the number and difficulty of the homework adequate?', - name: 'feedbackHomeworkQuestion2', - desc: '', - args: [], - ); - } - - /// `Did the homework / projects / practical activities help to understand the class?` - String get feedbackHomeworkQuestion3 { - return Intl.message( - 'Did the homework / projects / practical activities help to understand the class?', - name: 'feedbackHomeworkQuestion3', - desc: '', - args: [], - ); - } - - /// `What are the positive aspects of this class?` - String get feedbackPersonalQuestion1 { - return Intl.message( - 'What are the positive aspects of this class?', - name: 'feedbackPersonalQuestion1', - desc: '', - args: [], - ); - } - - /// `What do you think needs to be improved in this class?` - String get feedbackPersonalQuestion2 { - return Intl.message( - 'What do you think needs to be improved in this class?', - name: 'feedbackPersonalQuestion2', - desc: '', - args: [], - ); - } - - /// `In your opinion, the main difficulty in pursuing this class comes from:` - String get feedbackPersonalQuestion3 { - return Intl.message( - 'In your opinion, the main difficulty in pursuing this class comes from:', - name: 'feedbackPersonalQuestion3', - desc: '', - args: [], - ); - } - - /// `Other personal comments or suggestions regarding the activities carried out in this discipline:` - String get feedbackPersonalQuestion4 { - return Intl.message( - 'Other personal comments or suggestions regarding the activities carried out in this discipline:', - name: 'feedbackPersonalQuestion4', - desc: '', - args: [], - ); - } - /// `@stud.acs.upb.ro` String get stringEmailDomain { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2840689bd..5d8e14eef 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -285,28 +285,6 @@ "infoExportToGoogleCalendar": "Export filtered events from Timetable", "infoFormAnonymous": "This form is anonymous.", - "feedbackGeneralQuestion1": "Is your overall assessment of this discipline positive?", - "feedbackGeneralQuestion2": "What grade do you expect to get in this class? (1-10)", - "feedbackGeneralQuestion3": "Is the overall load in this class lower than in other classes that offer the same number of credits?", - "feedbackGeneralQuestion4": "Is the endowment (location / hardware and software / digital support) adequate for the activities of this discipline?", - "feedbackActivitiesQuestion": "Approximate number of activities that you attended (lectures + applications):", - "feedbackLectureQuestion1": "Does the teacher master the field of study well?", - "feedbackLectureApplicationsQuestion2": "Was the exposure method appropriate?", - "feedbackLectureQuestion3": "Did the lecture stimulate discussions and did the teacher clearly answer students' questions?", - "feedbackLectureQuestion4": "Was the teacher's behaviour towards students appropriate?", - "feedbackLectureQuestion5": "Are the teaching materials provided sufficient to understand the lecture?", - "feedbackApplicationsQuestion1": "Does the assistant master the field of study well?", - "feedbackApplicationsQuestion2": "Did the applications stimulate discussions and did the assistant clearly answer students' questions?", - "feedbackApplicationsQuestion3": "Was the assistant's behaviour towards students appropriate?", - "feedbackApplicationsQuestion4": "Are the teaching materials provided sufficient to understand the applications?", - "feedbackHomeworkQuestion1": "Estimate the average number of hours per week devoted to solving homework (between 1 and 10 hours).", - "feedbackHomeworkQuestion2": "Were the number and difficulty of the homework adequate?", - "feedbackHomeworkQuestion3": "Did the homework / projects / practical activities help to understand the class?", - "feedbackPersonalQuestion1": "What are the positive aspects of this class?", - "feedbackPersonalQuestion2": "What do you think needs to be improved in this class?", - "feedbackPersonalQuestion3": "In your opinion, the main difficulty in pursuing this class comes from:", - "feedbackPersonalQuestion4": "Other personal comments or suggestions regarding the activities carried out in this discipline:", - "stringEmailDomain": "@stud.acs.upb.ro", "stringForum": "cs.curs.pub.ro", "stringAnonymous": "Anonymous", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 62573f905..e81feb43f 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -286,28 +286,6 @@ "infoExportToGoogleCalendar": "Exportă evenimentele filtrate din Orar", "infoFormAnonymous": "Acest formular este anonim.", - "feedbackGeneralQuestion1": "Evaluarea dumneavoastră generală cu privire la această disciplină este pozitivă?", - "feedbackGeneralQuestion2": "Care este nota pe care vă așteptați să o obțineți la această disciplină? (1-10)", - "feedbackGeneralQuestion3": "Încărcarea generală la această disciplină este mai mică decât cea a altor discipline care oferă același număr de credite?", - "feedbackGeneralQuestion4": "Dotarea (locație/echipamente hardware și software/suport digital) este adecvată activităților acestei discipline?", - "feedbackActivitiesQuestion": "Numărul aproximativ de activități la care ați participat (curs + aplicații):", - "feedbackLectureQuestion1": "Cadrul didactic stăpânește bine domeniul de studiu?", - "feedbackLectureApplicationsQuestion2": "Metoda de expunere a fost potrivită?", - "feedbackLectureQuestion3": "Cursul a stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?", - "feedbackLectureQuestion4": "Comportamentul cadrului didactic față de studenți a fost adecvat?", - "feedbackLectureQuestion5": "Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea cursului?", - "feedbackApplicationsQuestion1": "Cadrul didactic stăpânește bine domeniul de studiu?", - "feedbackApplicationsQuestion2": "Aplicațiile au stimulat discuțiile și cadrul didactic a răspuns clar întrebărilor studenților?", - "feedbackApplicationsQuestion3": "Comportamentul cadrului didactic față de studenți a fost adecvat?", - "feedbackApplicationsQuestion4": "Materialele didactice puse la dispoziție sunt suficiente pentru înțelegerea aplicațiilor?", - "feedbackHomeworkQuestion1": "Estimați numărul mediu de ore pe săptămână dedicate rezolvării temelor (un număr între 1 și 10 ore).", - "feedbackHomeworkQuestion2": "Numărul și dificultatea temelor au fost adecvate?", - "feedbackHomeworkQuestion3": "Temele/proiectele/activitățile practice au ajutat la înțelegerea materiei?", - "feedbackPersonalQuestion1": "Care sunt aspectele pozitive ale acestei discipline?", - "feedbackPersonalQuestion2": "Ce considerați că trebuie îmbunătățit la această disciplină?", - "feedbackPersonalQuestion3": "După părerea dumneavoastră, dificultatea principală în urmărirea acestei discipline provine din:", - "feedbackPersonalQuestion4": "Alte comentarii personale sau sugestii referitoare la activitățile desfășurate la această disciplină:", - "stringEmailDomain": "@stud.acs.upb.ro", "stringForum": "cs.curs.pub.ro", "stringAnonymous": "Anonim", diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index c41f1f19a..275cc43b2 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -35,8 +35,9 @@ class _ClassFeedbackViewState extends State { Person selectedAssistant; List classTeachers = []; List feedbackQuestions = []; - Map> responses = {}; + List responses = []; TextEditingController gradeController; + List> initialValues = []; Map emojiSelected = { 0: false, @@ -65,6 +66,17 @@ class _ClassFeedbackViewState extends State { Provider.of(context, listen: false) .fetchQuestions() .then((questions) => setState(() => feedbackQuestions = questions)); + + for (var i = 0; i < 22; i++) { + responses.insert(i, '-1'); + initialValues.insert(i, { + 0: false, + 1: false, + 2: false, + 3: false, + 4: false, + }); + } } Widget autocompleteAssistant(BuildContext context) { @@ -241,14 +253,17 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - //generalQuestionsInput.single, - S.current.feedbackGeneralQuestion2, + generalQuestionsInput.isNotEmpty + ? generalQuestionsInput.single + : '-', style: const TextStyle( fontSize: 18, ), ), TextFormField( controller: gradeController, + autovalidateMode: + AutovalidateMode.onUserInteraction, decoration: InputDecoration( labelText: S.of(context).labelGrade, prefixIcon: const Icon(Icons.grade_outlined), @@ -265,14 +280,13 @@ class _ClassFeedbackViewState extends State { const SizedBox(height: 24), ...generalQuestionsRating.asMap().entries.map( (entry) { - final length = generalQuestionsRating.length; return EmojiFormField( question: entry.value, - questionIndex: entry.key, - responses: responses, onSaved: (value) { - //responses = responses; - //print(responses); + responses[entry.key] = value.keys + .firstWhere( + (element) => value[element] == true) + .toString(); }, validator: (selection) { if (selection.values @@ -284,7 +298,7 @@ class _ClassFeedbackViewState extends State { } return null; }, - initialValues: emojiSelected, + initialValues: initialValues[entry.key], ); }, ), @@ -304,8 +318,9 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - //involvementQuestions.single, - S.current.feedbackActivitiesQuestion, + involvementQuestions.isNotEmpty + ? involvementQuestions.single + : '-', style: const TextStyle( fontSize: 18, ), @@ -355,6 +370,16 @@ class _ClassFeedbackViewState extends State { ...lectureQuestions.asMap().entries.map((entry) { return EmojiFormField( question: entry.value, + onSaved: (value) { + responses[ + generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + entry.key] = value.keys + .firstWhere( + (element) => value[element] == true) + .toString(); + }, validator: (selection) { if (selection.values .where((e) => e != false) @@ -365,7 +390,11 @@ class _ClassFeedbackViewState extends State { } return null; }, - initialValues: emojiSelected, + initialValues: initialValues[ + generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + entry.key], ); }), ], @@ -382,25 +411,40 @@ class _ClassFeedbackViewState extends State { S.of(context).sectionApplications, style: Theme.of(context).textTheme.headline6, ), - ...applicationsQuestions - .asMap() - .entries - .map((entry) { - return EmojiFormField( - question: entry.value, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: emojiSelected, - ); - }), + ...applicationsQuestions.asMap().entries.map( + (entry) { + return EmojiFormField( + question: entry.value, + onSaved: (value) { + responses[ + generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + lectureQuestions.length + + entry.key] = value.keys + .firstWhere( + (element) => value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: initialValues[ + generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + lectureQuestions.length + + entry.key], + ); + }, + ), ], ), ), @@ -417,8 +461,9 @@ class _ClassFeedbackViewState extends State { ), const SizedBox(height: 24), Text( - //homeworkQuestionsInput.single, - S.current.feedbackHomeworkQuestion1, + homeworkQuestionsInput.isNotEmpty + ? homeworkQuestionsInput.single + : '-', style: const TextStyle( fontSize: 18, ), @@ -436,16 +481,6 @@ class _ClassFeedbackViewState extends State { .map((entry) { return EmojiFormField( question: entry.value, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, initialValues: emojiSelected, ); }), @@ -517,25 +552,49 @@ class _ClassFeedbackViewState extends State { return; } - final response = ClassFeedbackQuestionAnswer( - assistant: selectedAssistant, - teacherName: selectedTeacherName, - className: classController.text, - questionNumber: '0', - questionNumericAnswer: gradeController.text.toString(), - ); - //print(responses); - final res = - await Provider.of(context, listen: false) + setState(() { + formKey.currentState.save(); + print(responses); + }); + + bool res = false; + + for (var i = 0; i <= 14; i++) { + if (i == 1 || i == 4) { + final answer = i == 1 + ? gradeController.text.toString() + : selectedInvolvement; + + final response = ClassFeedbackQuestionAnswer( + assistant: selectedAssistant, + teacherName: selectedTeacherName, + className: classController.text, + questionNumber: i.toString(), + questionTextAnswer: answer); + + res = await Provider.of(context, listen: false) .addResponse(response); + continue; + } + final response = ClassFeedbackQuestionAnswer( + assistant: selectedAssistant, + teacherName: selectedTeacherName, + className: classController.text, + questionNumber: i.toString(), + questionNumericAnswer: i == 0 || i >= 5 + ? responses.elementAt(i) + : responses.elementAt(i - 1), + ); + + res = await Provider.of(context, listen: false) + .addResponse(response); + + //if (!res) break; + } if (res) { Navigator.of(context).pop(); AppToast.show(S.current.messageEventAdded); } - setState(() { - //print(responses); - formKey.currentState.save(); - }); }, ); } diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 553a78a16..ccf256fa0 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -7,8 +7,6 @@ class EmojiFormField extends FormField> { @required String question, FormFieldSetter> onSaved, String Function(Map) validator, - int questionIndex, - Map> responses, Key key, }) : super( key: key, @@ -70,17 +68,11 @@ class EmojiFormField extends FormField> { controller.select(); state.value[i] = selected; state.didChange(state.value); - responses.addAll({questionIndex: null}); - responses.update( - questionIndex, (value) => state.value); //print('2. ${state.value}'); } else { controller.deselect(); state.value[i] = selected; state.didChange(state.value); - responses.addAll({questionIndex: null}); - responses.update( - questionIndex, (value) => state.value); //print('3. ${state.value}'); } }, From fcc64bfc42577445653feee44f30b01d76020962 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Fri, 7 May 2021 02:27:32 +0300 Subject: [PATCH 14/59] Save all questions' answers --- .../view/class_feedback_view.dart | 729 ++++++++++-------- lib/widgets/radio_emoji.dart | 3 - 2 files changed, 395 insertions(+), 337 deletions(-) diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 275cc43b2..c23c2eba0 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -37,22 +37,16 @@ class _ClassFeedbackViewState extends State { List feedbackQuestions = []; List responses = []; TextEditingController gradeController; + TextEditingController hoursController; List> initialValues = []; - Map emojiSelected = { - 0: false, - 1: false, - 2: false, - 3: false, - 4: false, - }; - @override void initState() { super.initState(); classController = TextEditingController(text: widget.classHeader?.id ?? ''); gradeController = TextEditingController(); + hoursController = TextEditingController(); involvementPercentages = [ '0% ... 20%', '20% ... 40%', @@ -180,358 +174,421 @@ class _ClassFeedbackViewState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Column(children: [ - IconText( - icon: Icons.info_outline, - text: S.of(context).infoFormAnonymous, - ), - TextFormField( - enabled: false, - controller: classController, - decoration: InputDecoration( - labelText: S.of(context).labelClass, - prefixIcon: const Icon(FeatherIcons.bookOpen), + Column( + children: [ + IconText( + icon: Icons.info_outline, + text: S.of(context).infoFormAnonymous, ), - onChanged: (_) => setState(() {}), - ), - FutureBuilder( - future: personProvider - .mostRecentLecturer(widget.classHeader.id), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - final lecturerName = snapshot.data; - selectedTeacherName = lecturerName; - return TextFormField( - enabled: false, - controller: TextEditingController( - text: lecturerName ?? '-'), - decoration: InputDecoration( - labelText: S.of(context).labelLecturer, - prefixIcon: const Icon(Icons.person_outline), - ), - onChanged: (_) => setState(() {}), - ); - } else { - return const Center( - child: CircularProgressIndicator()); - } - }, - ), - autocompleteAssistant(context), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Checkbox( - value: agreedToResponsibilities, - visualDensity: VisualDensity.compact, - onChanged: (value) => setState( - () => agreedToResponsibilities = value), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 10.25), - child: Text( - S.of(context).messageAgreeFeedbackPolicy, - style: Theme.of(context).textTheme.subtitle1, - ), - ), - ), - ], + TextFormField( + enabled: false, + controller: classController, + decoration: InputDecoration( + labelText: S.of(context).labelClass, + prefixIcon: const Icon(FeatherIcons.bookOpen), + ), + onChanged: (_) => setState(() {}), ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionGeneralQuestions, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - generalQuestionsInput.isNotEmpty - ? generalQuestionsInput.single - : '-', - style: const TextStyle( - fontSize: 18, - ), - ), - TextFormField( - controller: gradeController, - autovalidateMode: - AutovalidateMode.onUserInteraction, + FutureBuilder( + future: personProvider + .mostRecentLecturer(widget.classHeader.id), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + final lecturerName = snapshot.data; + selectedTeacherName = lecturerName; + return TextFormField( + enabled: false, + controller: TextEditingController( + text: lecturerName ?? '-'), decoration: InputDecoration( - labelText: S.of(context).labelGrade, - prefixIcon: const Icon(Icons.grade_outlined), + labelText: S.of(context).labelLecturer, + prefixIcon: const Icon(Icons.person_outline), ), - validator: (value) { - if (value?.isEmpty ?? true) { - return S - .current.warningYouNeedToSelectAtLeastOne; - } - return null; - }, onChanged: (_) => setState(() {}), + ); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + ), + autocompleteAssistant(context), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: agreedToResponsibilities, + visualDensity: VisualDensity.compact, + onChanged: (value) => setState( + () => agreedToResponsibilities = value), ), - const SizedBox(height: 24), - ...generalQuestionsRating.asMap().entries.map( - (entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[entry.key] = value.keys - .firstWhere( - (element) => value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: initialValues[entry.key], - ); - }, + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10.25), + child: Text( + S.of(context).messageAgreeFeedbackPolicy, + style: Theme.of(context).textTheme.subtitle1, + ), + ), ), ], ), ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionInvolvement, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - involvementQuestions.isNotEmpty - ? involvementQuestions.single - : '-', - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionGeneralQuestions, + style: Theme.of(context).textTheme.headline6, ), - ), - DropdownButtonFormField( - decoration: InputDecoration( - labelText: S.of(context).sectionInvolvement, - prefixIcon: - const Icon(Icons.local_activity_outlined), + const SizedBox(height: 24), + Text( + generalQuestionsInput.isNotEmpty + ? generalQuestionsInput.single + : '-', + style: const TextStyle( + fontSize: 18, + ), ), - value: selectedInvolvement, - items: involvementPercentages - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type.toString()), - ), - ) - .toList(), - onChanged: (selection) { - formKey.currentState.validate(); - setState(() => selectedInvolvement = selection); - }, - validator: (selection) { - if (selection == null) { - return S - .of(context) - .errorEventTypeCannotBeEmpty; - } - return null; - }, - ), - ], + TextFormField( + controller: gradeController, + autovalidateMode: + AutovalidateMode.onUserInteraction, + decoration: InputDecoration( + labelText: S.of(context).labelGrade, + prefixIcon: const Icon(Icons.grade_outlined), + ), + validator: (value) { + if (value?.isEmpty ?? true) { + return S.current + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + ...generalQuestionsRating.asMap().entries.map( + (entry) { + return EmojiFormField( + question: entry.value, + onSaved: (value) { + responses[entry.key] = value.keys + .firstWhere((element) => + value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: initialValues[entry.key], + ); + }, + ), + ], + ), ), ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).uniEventTypeLecture, - style: Theme.of(context).textTheme.headline6, - ), - ...lectureQuestions.asMap().entries.map((entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[ - generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - entry.key] = value.keys - .firstWhere( - (element) => value[element] == true) - .toString(); + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionInvolvement, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + Text( + involvementQuestions.isNotEmpty + ? involvementQuestions.single + : '-', + style: const TextStyle( + fontSize: 18, + ), + ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.of(context).sectionInvolvement, + prefixIcon: + const Icon(Icons.local_activity_outlined), + ), + value: selectedInvolvement, + items: involvementPercentages + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState( + () => selectedInvolvement = selection); }, validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { + if (selection == null) { return S .of(context) - .warningYouNeedToSelectAtLeastOne; + .errorEventTypeCannotBeEmpty; } return null; }, - initialValues: initialValues[ - generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - entry.key], - ); - }), - ], + ), + ], + ), ), ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionApplications, - style: Theme.of(context).textTheme.headline6, - ), - ...applicationsQuestions.asMap().entries.map( - (entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[ + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).uniEventTypeLecture, + style: Theme.of(context).textTheme.headline6, + ), + ...lectureQuestions.asMap().entries.map( + (entry) { + return EmojiFormField( + question: entry.value, + onSaved: (value) { + responses[generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + entry.key] = + value.keys + .firstWhere((element) => + value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: initialValues[ + generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + entry.key], + ); + }, + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionApplications, + style: Theme.of(context).textTheme.headline6, + ), + ...applicationsQuestions.asMap().entries.map( + (entry) { + return EmojiFormField( + question: entry.value, + onSaved: (value) { + responses[generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + lectureQuestions.length + + entry.key] = + value.keys + .firstWhere((element) => + value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: initialValues[ generalQuestionsInput.length + generalQuestionsRating.length + involvementQuestions.length + lectureQuestions.length + - entry.key] = value.keys - .firstWhere( - (element) => value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: initialValues[ - generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - lectureQuestions.length + - entry.key], - ); - }, - ), - ], + entry.key], + ); + }, + ), + ], + ), ), ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).uniEventTypeHomework, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - homeworkQuestionsInput.isNotEmpty - ? homeworkQuestionsInput.single - : '-', - style: const TextStyle( - fontSize: 18, + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).uniEventTypeHomework, + style: Theme.of(context).textTheme.headline6, ), - ), - TextFormField( - decoration: InputDecoration( - labelText: S.of(context).labelGrade, - prefixIcon: const Icon(Icons.grade_outlined), + const SizedBox(height: 24), + Text( + homeworkQuestionsInput.isNotEmpty + ? homeworkQuestionsInput.single + : '-', + style: const TextStyle( + fontSize: 18, + ), ), - onChanged: (_) => setState(() {}), - ), - ...homeworkQuestionsRating - .asMap() - .entries - .map((entry) { - return EmojiFormField( - question: entry.value, - initialValues: emojiSelected, - ); - }), - ], + TextFormField( + controller: hoursController, + autovalidateMode: + AutovalidateMode.onUserInteraction, + decoration: InputDecoration( + labelText: S.current.labelGrade, + prefixIcon: const Icon(Icons.grade_outlined), + ), + validator: (value) { + if (value?.isEmpty ?? true) { + return S.current + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + onChanged: (_) => setState(() {}), + ), + ...homeworkQuestionsRating.asMap().entries.map( + (entry) { + return EmojiFormField( + question: entry.value, + onSaved: (value) { + responses[generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + lectureQuestions.length + + applicationsQuestions.length + + homeworkQuestionsInput.length + + entry.key] = + value.keys + .firstWhere((element) => + value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: initialValues[ + generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + lectureQuestions.length + + applicationsQuestions.length + + homeworkQuestionsInput.length + + entry.key], + ); + }, + ), + ], + ), ), ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionPersonalComments, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - ...personalQuestions.asMap().entries.map((entry) { - return Column( - children: [ - Text( - entry.value, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - keyboardType: - TextInputType.multiline, - maxLines: null, + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Text( + S.of(context).sectionPersonalComments, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + ...personalQuestions.asMap().entries.map( + (entry) { + return Column( + children: [ + Text( + entry.value, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + onSaved: (value) { + responses[generalQuestionsInput + .length + + generalQuestionsRating + .length + + involvementQuestions + .length + + lectureQuestions.length + + applicationsQuestions + .length + + homeworkQuestionsInput + .length + + homeworkQuestionsRating + .length + + entry.key] = value; + }, + keyboardType: + TextInputType.multiline, + maxLines: null, + ), + ], ), - ], + ), ), - ), - ), - const SizedBox(height: 24), - ], - ); - }), - ], + const SizedBox(height: 24), + ], + ); + }, + ), + ], + ), ), ), - ), - ]), + ], + ), ], ), ), @@ -559,11 +616,13 @@ class _ClassFeedbackViewState extends State { bool res = false; - for (var i = 0; i <= 14; i++) { - if (i == 1 || i == 4) { + for (var i = 0; i <= 21; i++) { + if (i == 1 || i == 4 || i == 15) { final answer = i == 1 ? gradeController.text.toString() - : selectedInvolvement; + : i == 4 + ? selectedInvolvement + : hoursController.text.toString(); final response = ClassFeedbackQuestionAnswer( assistant: selectedAssistant, @@ -577,14 +636,16 @@ class _ClassFeedbackViewState extends State { continue; } final response = ClassFeedbackQuestionAnswer( - assistant: selectedAssistant, - teacherName: selectedTeacherName, - className: classController.text, - questionNumber: i.toString(), - questionNumericAnswer: i == 0 || i >= 5 - ? responses.elementAt(i) - : responses.elementAt(i - 1), - ); + assistant: selectedAssistant, + teacherName: selectedTeacherName, + className: classController.text, + questionNumber: i.toString(), + questionNumericAnswer: i >= 18 + ? null + : i == 0 || i >= 5 || (i >= 16 && i <= 17) + ? responses.elementAt(i) + : responses.elementAt(i - 1), + questionTextAnswer: i >= 18 ? responses.elementAt(i) : null); res = await Provider.of(context, listen: false) .addResponse(response); diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index ccf256fa0..2834a723b 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -59,7 +59,6 @@ class EmojiFormField extends FormField> { for (final c in emojiControllers) { if (c != controller) { c.deselect(); - //TODO state.value[i] = selected; state.value[emojiControllers.indexOf(c)] = !selected; @@ -68,12 +67,10 @@ class EmojiFormField extends FormField> { controller.select(); state.value[i] = selected; state.didChange(state.value); - //print('2. ${state.value}'); } else { controller.deselect(); state.value[i] = selected; state.didChange(state.value); - //print('3. ${state.value}'); } }, ), From dd11bc2e0ce5d99a976c3434f962735e1dd396c7 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 9 May 2021 11:22:50 +0300 Subject: [PATCH 15/59] Minor code improvements --- lib/generated/intl/messages_en.dart | 4 +++- lib/generated/intl/messages_ro.dart | 2 ++ lib/generated/l10n.dart | 24 +++++++++++++++++-- lib/l10n/intl_en.arb | 4 +++- lib/l10n/intl_ro.arb | 2 ++ .../view/class_feedback_view.dart | 15 ++++++------ 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index aa73d536e..473390847 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -146,6 +146,7 @@ class MessageLookup extends MessageLookupByLibrary { "labelEnd" : MessageLookupByLibrary.simpleMessage("End"), "labelEvaluation" : MessageLookupByLibrary.simpleMessage("Evaluation"), "labelEven" : MessageLookupByLibrary.simpleMessage("Even"), + "labelFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("feedback policy"), "labelFirstName" : MessageLookupByLibrary.simpleMessage("First name"), "labelGrade" : MessageLookupByLibrary.simpleMessage("Grade"), "labelLastName" : MessageLookupByLibrary.simpleMessage("Last name"), @@ -199,6 +200,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Event added successfully."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Event deleted successfully."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Event modified successfully."), + "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("The review has been sent successfully."), "messageGetStartedByPressing" : MessageLookupByLibrary.simpleMessage("Get started by pressing the"), "messageIAgreeToThe" : MessageLookupByLibrary.simpleMessage("I agree to the "), "messageNewUser" : MessageLookupByLibrary.simpleMessage("New user?"), @@ -206,7 +208,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageNotLoggedIn" : MessageLookupByLibrary.simpleMessage("You need to be logged in to perform this action."), "messagePictureUpdatedSuccess" : MessageLookupByLibrary.simpleMessage("Profile picture updated successfully."), "messageRequestAlreadyExists" : MessageLookupByLibrary.simpleMessage("You have already submitted a request. If you want to add another one, please press \'Send\'."), - "messageRequestHasBeenSent" : MessageLookupByLibrary.simpleMessage("The request has been sent succesfully."), + "messageRequestHasBeenSent" : MessageLookupByLibrary.simpleMessage("The request has been sent successfully."), "messageResetPassword" : MessageLookupByLibrary.simpleMessage("Enter your e-mai in order to receive instructions on how to reset your password."), "messageShortcutDeleted" : MessageLookupByLibrary.simpleMessage("Shortcut deleted successfully."), "messageTapForMoreInfo" : MessageLookupByLibrary.simpleMessage("Tap for more info"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 7d449bc30..45b2c5424 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -146,6 +146,7 @@ class MessageLookup extends MessageLookupByLibrary { "labelEnd" : MessageLookupByLibrary.simpleMessage("Sfârșit"), "labelEvaluation" : MessageLookupByLibrary.simpleMessage("Evaluare"), "labelEven" : MessageLookupByLibrary.simpleMessage("Pară"), + "labelFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("politica de feedback"), "labelFirstName" : MessageLookupByLibrary.simpleMessage("Prenume"), "labelGrade" : MessageLookupByLibrary.simpleMessage("Notă"), "labelLastName" : MessageLookupByLibrary.simpleMessage("Nume"), @@ -199,6 +200,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Eveniment adăugat cu succes."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Eveniment șters cu succes."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Eveniment modificat cu succes."), + "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("Recenzia a fost trimisă cu succes."), "messageGetStartedByPressing" : MessageLookupByLibrary.simpleMessage("Începeți prin a apăsa butonul"), "messageIAgreeToThe" : MessageLookupByLibrary.simpleMessage("Sunt de acord cu "), "messageNewUser" : MessageLookupByLibrary.simpleMessage("Utilizator nou?"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index aef127e96..d27d1b013 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -445,6 +445,16 @@ class S { ); } + /// `feedback policy` + String get labelFeedbackPolicy { + return Intl.message( + 'feedback policy', + name: 'labelFeedbackPolicy', + desc: '', + args: [], + ); + } + /// `Last updated` String get labelLastUpdated { return Intl.message( @@ -2455,16 +2465,26 @@ class S { ); } - /// `The request has been sent succesfully.` + /// `The request has been sent successfully.` String get messageRequestHasBeenSent { return Intl.message( - 'The request has been sent succesfully.', + 'The request has been sent successfully.', name: 'messageRequestHasBeenSent', desc: '', args: [], ); } + /// `The review has been sent successfully.` + String get messageFeedbackHasBeenSent { + return Intl.message( + 'The review has been sent successfully.', + name: 'messageFeedbackHasBeenSent', + desc: '', + args: [], + ); + } + /// `You have already submitted a request. If you want to add another one, please press 'Send'.` String get messageRequestAlreadyExists { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5d8e14eef..3a32352ad 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -41,6 +41,7 @@ "labelPrivacyPolicy": "Privacy Policy", "labelPersonalInformation": "Personal information", "labelPermissionsConsent": "consent for editing rights", + "labelFeedbackPolicy": "feedback policy", "labelLastUpdated": "Last updated", "labelUniversityYear": "University year", "labelWeek": "Week", @@ -256,7 +257,8 @@ "messageGetStartedByPressing": "Get started by pressing the", "messageButtonAbove": "button above", "messageAskPermissionToEdit": "Why do you want edit permissions for {appName}?", - "messageRequestHasBeenSent": "The request has been sent succesfully.", + "messageRequestHasBeenSent": "The request has been sent successfully.", + "messageFeedbackHasBeenSent": "The review has been sent successfully.", "messageRequestAlreadyExists": "You have already submitted a request. If you want to add another one, please press 'Send'.", "messageIAgreeToThe": "I agree to the ", "messageEditProfileSuccess": "Profile updated successfully.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index e81feb43f..fe207c762 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -41,6 +41,7 @@ "labelPrivacyPolicy": "Politică de confidențialitate", "labelPersonalInformation": "Informații personale", "labelPermissionsConsent": "consimțământul pentru drepturi de editare", + "labelFeedbackPolicy": "politica de feedback", "labelLastUpdated": "Ultima modificare", "labelUniversityYear": "An universitar", "labelWeek": "Săptămână", @@ -257,6 +258,7 @@ "messageButtonAbove": "de mai sus.", "messageAskPermissionToEdit": "De ce dorești să primești permisiuni de editare în {appName}?", "messageRequestHasBeenSent": "Cererea a fost transmisă cu succes", + "messageFeedbackHasBeenSent": "Recenzia a fost trimisă cu succes.", "messageRequestAlreadyExists": "Ați trimis deja o cerere. Daca doriți să adăugați una nouă, vă rugăm sa apasați 'Salvare'.", "messageIAgreeToThe": "Sunt de acord cu ", "messageEditProfileSuccess": "Profilul a fost actualizat cu succes.", diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index c23c2eba0..33eda4ae3 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -27,7 +27,7 @@ class ClassFeedbackView extends StatefulWidget { class _ClassFeedbackViewState extends State { final formKey = GlobalKey(); TextEditingController classController; - bool agreedToResponsibilities = false; + bool agreedToResponsibilities; List involvementPercentages = []; String selectedInvolvement; @@ -44,6 +44,7 @@ class _ClassFeedbackViewState extends State { void initState() { super.initState(); + agreedToResponsibilities = false; classController = TextEditingController(text: widget.classHeader?.id ?? ''); gradeController = TextEditingController(); hoursController = TextEditingController(); @@ -57,11 +58,12 @@ class _ClassFeedbackViewState extends State { Provider.of(context, listen: false) .fetchPeople() .then((teachers) => setState(() => classTeachers = teachers)); + Provider.of(context, listen: false) .fetchQuestions() .then((questions) => setState(() => feedbackQuestions = questions)); - for (var i = 0; i < 22; i++) { + for (int i = 0; i < 22; i++) { responses.insert(i, '-1'); initialValues.insert(i, { 0: false, @@ -599,24 +601,23 @@ class _ClassFeedbackViewState extends State { } AppScaffoldAction _submitButton() => AppScaffoldAction( - text: 'Submit', + text: S.current.buttonSend, onPressed: () async { if (!formKey.currentState.validate()) return; if (!agreedToResponsibilities) { AppToast.show( - '${S.current.warningAgreeTo}${S.current.labelPermissionsConsent}.'); + '${S.current.warningAgreeTo}${S.current.labelFeedbackPolicy}.'); return; } setState(() { formKey.currentState.save(); - print(responses); }); bool res = false; - for (var i = 0; i <= 21; i++) { + for (var i = 0; i < feedbackQuestions.length; i++) { if (i == 1 || i == 4 || i == 15) { final answer = i == 1 ? gradeController.text.toString() @@ -654,7 +655,7 @@ class _ClassFeedbackViewState extends State { } if (res) { Navigator.of(context).pop(); - AppToast.show(S.current.messageEventAdded); + AppToast.show(S.current.messageFeedbackHasBeenSent); } }, ); From 627e9c3a1b0006bf13a7f0e2166e472a4fae3d4f Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 9 May 2021 18:22:31 +0300 Subject: [PATCH 16/59] Better generalize questions/answers --- .../service/feedback_provider.dart | 6 +- .../view/class_feedback_view.dart | 135 +++++++++++------- 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 290a28a6d..18713bee9 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -28,7 +28,6 @@ class FeedbackProvider with ChangeNotifier { .doc('class_feedback_answers') .collection(response.questionNumber) .add(response.toData()); - //notifyListeners(); return true; } catch (e) { print(e); @@ -37,15 +36,14 @@ class FeedbackProvider with ChangeNotifier { } } - Future> fetchQuestions() async { + Future> fetchQuestions() async { try { final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance .collection('forms') .doc('class_feedback_questions') .get(); final Map data = documentSnapshot['questions']; - final List values = data.values.toList(); - return values; + return data; } catch (e) { print(e); AppToast.show(S.current.errorSomethingWentWrong); diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 33eda4ae3..16093ac98 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -30,14 +30,11 @@ class _ClassFeedbackViewState extends State { bool agreedToResponsibilities; List involvementPercentages = []; - String selectedInvolvement; String selectedTeacherName; Person selectedAssistant; List classTeachers = []; - List feedbackQuestions = []; + Map feedbackQuestions = {}; List responses = []; - TextEditingController gradeController; - TextEditingController hoursController; List> initialValues = []; @override @@ -46,8 +43,6 @@ class _ClassFeedbackViewState extends State { agreedToResponsibilities = false; classController = TextEditingController(text: widget.classHeader?.id ?? ''); - gradeController = TextEditingController(); - hoursController = TextEditingController(); involvementPercentages = [ '0% ... 20%', '20% ... 40%', @@ -59,11 +54,15 @@ class _ClassFeedbackViewState extends State { .fetchPeople() .then((teachers) => setState(() => classTeachers = teachers)); - Provider.of(context, listen: false) + awaitFeedbackQuestions(); + } + + Future> awaitFeedbackQuestions() async { + await Provider.of(context, listen: false) .fetchQuestions() .then((questions) => setState(() => feedbackQuestions = questions)); - for (int i = 0; i < 22; i++) { + for (int i = 0; i <= feedbackQuestions.length; i++) { responses.insert(i, '-1'); initialValues.insert(i, { 0: false, @@ -73,6 +72,8 @@ class _ClassFeedbackViewState extends State { 4: false, }); } + + return feedbackQuestions; } Widget autocompleteAssistant(BuildContext context) { @@ -138,31 +139,43 @@ class _ClassFeedbackViewState extends State { final personProvider = Provider.of(context); final feedbackProvider = Provider.of(context); - final List generalQuestionsRating = feedbackProvider - .getQuestionsByCategoryAndType(feedbackQuestions, 'general', 'rating'); + final List generalQuestionsRating = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions.values.toList(), 'general', 'rating'); + + final List generalQuestionsRatingIndexes = feedbackQuestions.keys + .where((element) => + feedbackQuestions[element]['type'] == 'rating' && + feedbackQuestions[element]['category'] == 'general') + .toList(); - final List generalQuestionsInput = feedbackProvider - .getQuestionsByCategoryAndType(feedbackQuestions, 'general', 'input'); + final List generalQuestionsInput = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions.values.toList(), 'general', 'input'); final List involvementQuestions = feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions, 'involvement', 'input'); + feedbackQuestions.values.toList(), 'involvement', 'input'); - final List lectureQuestions = feedbackProvider - .getQuestionsByCategoryAndType(feedbackQuestions, 'lecture', 'rating'); + final List lectureQuestions = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions.values.toList(), 'lecture', 'rating'); final List applicationsQuestions = feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions, 'applications', 'rating'); + feedbackQuestions.values.toList(), 'applications', 'rating'); - final List homeworkQuestionsRating = feedbackProvider - .getQuestionsByCategoryAndType(feedbackQuestions, 'homework', 'rating'); + final List homeworkQuestionsRating = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions.values.toList(), 'homework', 'rating'); - final List homeworkQuestionsInput = feedbackProvider - .getQuestionsByCategoryAndType(feedbackQuestions, 'homework', 'input'); + final List homeworkQuestionsInput = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions.values.toList(), 'homework', 'input'); - final List personalQuestions = feedbackProvider - .getQuestionsByCategoryAndType(feedbackQuestions, 'personal', 'input'); + final List personalQuestions = + feedbackProvider.getQuestionsByCategoryAndType( + feedbackQuestions.values.toList(), 'personal', 'input'); return AppScaffold( title: Text(S.of(context).navigationClassFeedback), @@ -259,7 +272,19 @@ class _ClassFeedbackViewState extends State { ), ), TextFormField( - controller: gradeController, + onSaved: (value) { + responses[int.parse( + feedbackQuestions.keys.isNotEmpty + ? feedbackQuestions.keys.firstWhere( + (element) => + feedbackQuestions[element] + ['type'] == + 'input' && + feedbackQuestions[element] + ['category'] == + 'general') + : '-1')] = value; + }, autovalidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( @@ -281,7 +306,9 @@ class _ClassFeedbackViewState extends State { return EmojiFormField( question: entry.value, onSaved: (value) { - responses[entry.key] = value.keys + responses[int.parse( + generalQuestionsRatingIndexes[ + entry.key])] = value.keys .firstWhere((element) => value[element] == true) .toString(); @@ -329,7 +356,10 @@ class _ClassFeedbackViewState extends State { prefixIcon: const Icon(Icons.local_activity_outlined), ), - value: selectedInvolvement, + onSaved: (value) { + responses[generalQuestionsInput.length + + generalQuestionsRating.length] = value; + }, items: involvementPercentages .map( (type) => DropdownMenuItem( @@ -340,8 +370,7 @@ class _ClassFeedbackViewState extends State { .toList(), onChanged: (selection) { formKey.currentState.validate(); - setState( - () => selectedInvolvement = selection); + setState(() => {}); }, validator: (selection) { if (selection == null) { @@ -470,7 +499,13 @@ class _ClassFeedbackViewState extends State { ), ), TextFormField( - controller: hoursController, + onSaved: (value) { + responses[generalQuestionsInput.length + + generalQuestionsRating.length + + involvementQuestions.length + + lectureQuestions.length + + applicationsQuestions.length] = value; + }, autovalidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( @@ -615,43 +650,35 @@ class _ClassFeedbackViewState extends State { formKey.currentState.save(); }); - bool res = false; + bool res; for (var i = 0; i < feedbackQuestions.length; i++) { - if (i == 1 || i == 4 || i == 15) { - final answer = i == 1 - ? gradeController.text.toString() - : i == 4 - ? selectedInvolvement - : hoursController.text.toString(); - + res = false; + if (feedbackQuestions[i.toString()]['type'] == 'input') { final response = ClassFeedbackQuestionAnswer( - assistant: selectedAssistant, - teacherName: selectedTeacherName, - className: classController.text, - questionNumber: i.toString(), - questionTextAnswer: answer); + assistant: selectedAssistant, + teacherName: selectedTeacherName, + className: classController.text, + questionNumber: i.toString(), + questionTextAnswer: responses[i], + ); res = await Provider.of(context, listen: false) .addResponse(response); - continue; - } - final response = ClassFeedbackQuestionAnswer( + if (!res) break; + } else { + final response = ClassFeedbackQuestionAnswer( assistant: selectedAssistant, teacherName: selectedTeacherName, className: classController.text, questionNumber: i.toString(), - questionNumericAnswer: i >= 18 - ? null - : i == 0 || i >= 5 || (i >= 16 && i <= 17) - ? responses.elementAt(i) - : responses.elementAt(i - 1), - questionTextAnswer: i >= 18 ? responses.elementAt(i) : null); - - res = await Provider.of(context, listen: false) - .addResponse(response); + questionNumericAnswer: responses[i], + ); - //if (!res) break; + res = await Provider.of(context, listen: false) + .addResponse(response); + if (!res) break; + } } if (res) { Navigator.of(context).pop(); From 820a5d60314f5771f7729d017b99eb9893eb09d0 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 9 May 2021 22:53:44 +0300 Subject: [PATCH 17/59] Rename variables --- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ro.dart | 3 ++- lib/generated/l10n.dart | 10 ++++++++++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ro.arb | 3 ++- .../class_feedback/model/class_feedback_answer.dart | 4 ++-- .../class_feedback/service/feedback_provider.dart | 4 ++-- lib/pages/class_feedback/view/class_feedback_view.dart | 9 ++++----- 8 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 473390847..f8e8b64cd 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -317,6 +317,7 @@ class MessageLookup extends MessageLookupByLibrary { "warningUnableToReachNewsFeed" : MessageLookupByLibrary.simpleMessage("Unable to reach the news feed."), "warningUseProvider" : m12, "warningWebsiteNameExists" : MessageLookupByLibrary.simpleMessage("A website with the same name already exists."), + "warningYouNeedToSelectAssistant" : MessageLookupByLibrary.simpleMessage("You need to select your assistant for this class."), "warningYouNeedToSelectAtLeastOne" : MessageLookupByLibrary.simpleMessage("You need to select at least one option."), "websiteCategoryAdministrative" : MessageLookupByLibrary.simpleMessage("Administrative"), "websiteCategoryAssociations" : MessageLookupByLibrary.simpleMessage("Associations"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 45b2c5424..4ccd7bfda 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -223,7 +223,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Bine ai venit!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Cere permisiuni"), - "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Recenzie"), + "navigationClassFeedback" : MessageLookupByLibrary.simpleMessage("Feedback"), "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Informații materie"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Materii"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Detalii eveniment"), @@ -317,6 +317,7 @@ class MessageLookup extends MessageLookupByLibrary { "warningUnableToReachNewsFeed" : MessageLookupByLibrary.simpleMessage("Nu am putut încărca fluxul de știri."), "warningUseProvider" : m12, "warningWebsiteNameExists" : MessageLookupByLibrary.simpleMessage("Există deja un site cu același nume."), + "warningYouNeedToSelectAssistant" : MessageLookupByLibrary.simpleMessage("Trebuie să selectați asistentul de la această materie."), "warningYouNeedToSelectAtLeastOne" : MessageLookupByLibrary.simpleMessage("Trebuie să selectați cel puțin o opțiune."), "websiteCategoryAdministrative" : MessageLookupByLibrary.simpleMessage("Administrativ"), "websiteCategoryAssociations" : MessageLookupByLibrary.simpleMessage("Asociații"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index d27d1b013..17a936bce 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1545,6 +1545,16 @@ class S { ); } + /// `You need to select your assistant for this class.` + String get warningYouNeedToSelectAssistant { + return Intl.message( + 'You need to select your assistant for this class.', + name: 'warningYouNeedToSelectAssistant', + desc: '', + args: [], + ); + } + /// `Could not read favourite websites.` String get warningFavouriteWebsitesInitializationFailed { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3a32352ad..6df1833f0 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -157,6 +157,7 @@ "warningOnlyNOptionsAtATime": "Only {n} options can be selected at a time.", "warningNoEvents": "No events to show", "warningYouNeedToSelectAtLeastOne": "You need to select at least one option.", + "warningYouNeedToSelectAssistant": "You need to select your assistant for this class.", "warningFavouriteWebsitesInitializationFailed": "Could not read favourite websites.", "warningUnableToReachNewsFeed": "Unable to reach the news feed.", "warningNoNews": "There are no news yet.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index fe207c762..150755fa7 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -157,6 +157,7 @@ "warningOnlyNOptionsAtATime": "Doar {n} opțiuni pot fi selectate la un moment dat.", "warningNoEvents": "Nu există evenimente de afișat", "warningYouNeedToSelectAtLeastOne": "Trebuie să selectați cel puțin o opțiune.", + "warningYouNeedToSelectAssistant": "Trebuie să selectați asistentul de la această materie.", "warningFavouriteWebsitesInitializationFailed": "Nu se pot citi date despre site-urile favorite.", "warningUnableToReachNewsFeed": "Nu am putut încărca fluxul de știri.", "warningNoNews": "Nu au fost postate știri încă.", @@ -175,7 +176,7 @@ "navigationEventDetails": "Detalii eveniment", "navigationNewsFeed": "Știri", "navigationClassInfo": "Informații materie", - "navigationClassFeedback": "Recenzie", + "navigationClassFeedback": "Feedback", "filterMenuShowAll": "Arată tot", "filterMenuShowMine": "Arată doar pe ale mele", diff --git a/lib/pages/class_feedback/model/class_feedback_answer.dart b/lib/pages/class_feedback/model/class_feedback_answer.dart index 92b21f263..0bfe6105a 100644 --- a/lib/pages/class_feedback/model/class_feedback_answer.dart +++ b/lib/pages/class_feedback/model/class_feedback_answer.dart @@ -1,7 +1,7 @@ import 'package:acs_upb_mobile/pages/people/model/person.dart'; -class ClassFeedbackQuestionAnswer { - ClassFeedbackQuestionAnswer({ +class ClassFeedbackAnswer { + ClassFeedbackAnswer({ this.questionTextAnswer, this.questionNumericAnswer, this.className, diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 18713bee9..c506d893d 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -5,7 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -extension ClassFeedbackQuestionAnswerExtension on ClassFeedbackQuestionAnswer { +extension ClassFeedbackAnswerExtension on ClassFeedbackAnswer { Map toData() { final Map data = {}; @@ -21,7 +21,7 @@ extension ClassFeedbackQuestionAnswerExtension on ClassFeedbackQuestionAnswer { } class FeedbackProvider with ChangeNotifier { - Future addResponse(ClassFeedbackQuestionAnswer response) async { + Future addResponse(ClassFeedbackAnswer response) async { try { await FirebaseFirestore.instance .collection('forms') diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 16093ac98..b05ede056 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -34,7 +34,7 @@ class _ClassFeedbackViewState extends State { Person selectedAssistant; List classTeachers = []; Map feedbackQuestions = {}; - List responses = []; + Map responses = {}; List> initialValues = []; @override @@ -63,7 +63,6 @@ class _ClassFeedbackViewState extends State { .then((questions) => setState(() => feedbackQuestions = questions)); for (int i = 0; i <= feedbackQuestions.length; i++) { - responses.insert(i, '-1'); initialValues.insert(i, { 0: false, 1: false, @@ -96,7 +95,7 @@ class _ClassFeedbackViewState extends State { }, validator: (_) { if (textEditingController.text.isEmpty ?? true) { - return S.current.warningYouNeedToSelectAtLeastOne; + return S.current.warningYouNeedToSelectAssistant; } return null; }, @@ -655,7 +654,7 @@ class _ClassFeedbackViewState extends State { for (var i = 0; i < feedbackQuestions.length; i++) { res = false; if (feedbackQuestions[i.toString()]['type'] == 'input') { - final response = ClassFeedbackQuestionAnswer( + final response = ClassFeedbackAnswer( assistant: selectedAssistant, teacherName: selectedTeacherName, className: classController.text, @@ -667,7 +666,7 @@ class _ClassFeedbackViewState extends State { .addResponse(response); if (!res) break; } else { - final response = ClassFeedbackQuestionAnswer( + final response = ClassFeedbackAnswer( assistant: selectedAssistant, teacherName: selectedTeacherName, className: classController.text, From 589ed2f3510cfdc175bd196f3dc1ff397d96be9a Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 9 May 2021 23:06:20 +0300 Subject: [PATCH 18/59] Show feedback button only in debug mode --- lib/pages/classes/view/class_view.dart | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 09e4b241f..f8f605731 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -15,6 +15,7 @@ import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:positioned_tap_detector/positioned_tap_detector.dart'; @@ -49,16 +50,17 @@ class _ClassViewState extends State { return AppScaffold( title: Text(S.current.navigationClassInfo), actions: [ - AppScaffoldAction( - icon: Icons.rate_review_outlined, - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - ClassFeedbackView(classHeader: widget.classHeader), - ), - ); - }), + if (kReleaseMode == false) + AppScaffoldAction( + icon: Icons.rate_review_outlined, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + ClassFeedbackView(classHeader: widget.classHeader), + ), + ); + }), ], body: FutureBuilder( future: classProvider.fetchClassInfo(widget.classHeader), From c7e36357ac8180172843164f8c27bbc7bb626ce4 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Mon, 10 May 2021 19:51:14 +0300 Subject: [PATCH 19/59] Create custom AutocompletePerson widget --- .../view/class_feedback_view.dart | 70 ++------------ lib/pages/people/view/people_page.dart | 92 +++++++++++++++++++ .../timetable/view/events/add_event_view.dart | 64 ++----------- test/integration_test.dart | 13 ++- 4 files changed, 118 insertions(+), 121 deletions(-) diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index b05ede056..14c7d3d17 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -3,7 +3,7 @@ import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.da import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -import 'package:acs_upb_mobile/widgets/autocomplete.dart'; +import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; @@ -13,7 +13,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:recase/recase.dart'; class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -75,64 +74,6 @@ class _ClassFeedbackViewState extends State { return feedbackQuestions; } - Widget autocompleteAssistant(BuildContext context) { - return Autocomplete( - key: const Key('AutocompleteAssistant'), - fieldViewBuilder: (BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted) { - textEditingController.text = selectedAssistant?.name; - return TextFormField( - controller: textEditingController, - decoration: InputDecoration( - labelText: S.of(context).labelAssistant, - prefixIcon: const Icon(FeatherIcons.user), - ), - focusNode: focusNode, - onFieldSubmitted: (String value) { - onFieldSubmitted(); - }, - validator: (_) { - if (textEditingController.text.isEmpty ?? true) { - return S.current.warningYouNeedToSelectAssistant; - } - return null; - }, - ); - }, - displayStringForOption: (Person person) => person.name, - optionsBuilder: (TextEditingValue textEditingValue) { - if (textEditingValue.text == '' || textEditingValue.text.isEmpty) { - return const Iterable.empty(); - } - if (classTeachers.where((Person person) { - return person.name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }).isEmpty) { - final List inputTeachers = []; - final Person inputTeacher = - Person(name: textEditingValue.text.titleCase); - inputTeachers.add(inputTeacher); - return inputTeachers; - } - - return classTeachers.where((Person person) { - return person.name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }); - }, - onSelected: (Person selection) { - formKey.currentState.validate(); - setState(() { - selectedAssistant = selection; - }); - }, - ); - } - @override Widget build(BuildContext context) { final personProvider = Provider.of(context); @@ -227,7 +168,14 @@ class _ClassFeedbackViewState extends State { } }, ), - autocompleteAssistant(context), + AutocompletePerson( + key: const Key('AutocompleteAssistant'), + labelText: S.current.labelAssistant, + warning: S.current.warningYouNeedToSelectAssistant, + formKey: formKey, + onSaved: (value) => selectedAssistant = value, + classTeachers: classTeachers, + ), Padding( padding: const EdgeInsets.all(10), child: Row( diff --git a/lib/pages/people/view/people_page.dart b/lib/pages/people/view/people_page.dart index 434c10857..1c9e2ca09 100644 --- a/lib/pages/people/view/people_page.dart +++ b/lib/pages/people/view/people_page.dart @@ -2,12 +2,15 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; +import 'package:acs_upb_mobile/widgets/autocomplete.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/search_bar.dart'; import 'package:dynamic_text_highlighting/dynamic_text_highlighting.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; +import 'package:recase/recase.dart'; class PeoplePage extends StatefulWidget { const PeoplePage({Key key}) : super(key: key); @@ -138,3 +141,92 @@ class _PeopleListState extends State { ); } } + +class AutocompletePerson extends StatefulWidget { + const AutocompletePerson( + {@required this.labelText, + @required this.classTeachers, + Key key, + this.warning, + this.formKey, + this.onSaved, + this.personDisplayed}) + : super(key: key); + + final String labelText; + final String warning; + final GlobalKey formKey; + final List classTeachers; + final Person Function(Person) onSaved; + final Person personDisplayed; + + @override + _AutocompletePersonState createState() => _AutocompletePersonState(); +} + +class _AutocompletePersonState extends State { + Person selectedPerson; + + @override + Widget build(BuildContext context) { + return Autocomplete( + key: widget.key, + fieldViewBuilder: (BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted) { + textEditingController.text = selectedPerson?.name; + if (selectedPerson == null) { + textEditingController.text = widget.personDisplayed?.name; + } + return TextFormField( + controller: textEditingController, + decoration: InputDecoration( + labelText: widget.labelText, + prefixIcon: const Icon(FeatherIcons.user), + ), + focusNode: focusNode, + onFieldSubmitted: (String value) { + onFieldSubmitted(); + }, + validator: (_) { + if (textEditingController.text.isEmpty ?? true) { + return widget.warning; + } + return null; + }, + ); + }, + displayStringForOption: (Person person) => person.name, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '' || textEditingValue.text.isEmpty) { + return const Iterable.empty(); + } + if (widget.classTeachers.where((Person person) { + return person.name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }).isEmpty) { + final List inputTeachers = []; + final Person inputTeacher = + Person(name: textEditingValue.text.titleCase); + inputTeachers.add(inputTeacher); + return inputTeachers; + } + + return widget.classTeachers.where((Person person) { + return person.name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }); + }, + onSelected: (Person selection) { + widget.formKey.currentState.validate(); + setState(() { + selectedPerson = selection; + widget.onSaved(selectedPerson); + }); + }, + ); + } +} diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index cb52384a2..a61ed64d1 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -8,6 +8,7 @@ import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; import 'package:acs_upb_mobile/pages/filter/view/relevance_picker.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; @@ -16,7 +17,6 @@ import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; import 'package:acs_upb_mobile/resources/custom_icons.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/widgets/autocomplete.dart'; import 'package:acs_upb_mobile/widgets/button.dart'; import 'package:acs_upb_mobile/widgets/dialog.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; @@ -27,7 +27,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; -import 'package:recase/recase.dart'; import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart' as time_machine show DayOfWeek; import 'package:time_machine/time_machine.dart' hide DayOfWeek; @@ -190,58 +189,6 @@ class _AddEventViewState extends State { } } - Widget autocompleteLecturer(BuildContext context) { - return Autocomplete( - key: const Key('Autocomplete'), - fieldViewBuilder: (BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted) { - textEditingController.text = selectedTeacher?.name; - return TextFormField( - controller: textEditingController, - decoration: InputDecoration( - labelText: S.current.labelLecturer, - prefixIcon: const Icon(FeatherIcons.user), - ), - focusNode: focusNode, - onFieldSubmitted: (String value) { - onFieldSubmitted(); - }, - ); - }, - displayStringForOption: (Person person) => person.name, - optionsBuilder: (TextEditingValue textEditingValue) { - if (textEditingValue.text == '' || textEditingValue.text.isEmpty) { - return const Iterable.empty(); - } - if (classTeachers.where((Person person) { - return person.name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }).isEmpty) { - final List inputTeachers = []; - final Person inputTeacher = - Person(name: textEditingValue.text.titleCase); - inputTeachers.add(inputTeacher); - return inputTeachers; - } - - return classTeachers.where((Person person) { - return person.name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }); - }, - onSelected: (Person selection) { - formKey.currentState.validate(); - setState(() { - selectedTeacher = selection; - }); - }, - ); - } - @override Widget build(BuildContext context) { return AppScaffold( @@ -370,7 +317,14 @@ class _AddEventViewState extends State { }, ), if ([UniEventType.lecture].contains(selectedEventType)) - autocompleteLecturer(context), + AutocompletePerson( + key: const Key('AutocompleteLecturer'), + labelText: S.current.labelLecturer, + formKey: formKey, + onSaved: (value) => selectedTeacher = value, + classTeachers: classTeachers, + personDisplayed: selectedTeacher, + ), TextFormField( controller: locationController, decoration: InputDecoration( diff --git a/test/integration_test.dart b/test/integration_test.dart index 1b389f648..6080a52a1 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1082,7 +1082,8 @@ Future main() async { // Select lecturer - partial name await tester.tap(find.byIcon(FeatherIcons.user)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key('Autocomplete')), 'John'); + await tester.enterText( + find.byKey(const Key('AutocompleteLecturer')), 'John'); await tester.pumpAndSettle(); await tester.tap(find.text('John Doe')); await tester.pumpAndSettle(); @@ -1091,14 +1092,15 @@ Future main() async { await tester.tap(find.byIcon(FeatherIcons.user)); await tester.pumpAndSettle(); await tester.enterText( - find.byKey(const Key('Autocomplete')), 'Isabel Steward'); + find.byKey(const Key('AutocompleteLecturer')), 'Isabel Steward'); await tester.tap(find.text('Isabel Steward')); await tester.pumpAndSettle(); // Select lecturer - check autocomplete suggestions await tester.tap(find.byIcon(FeatherIcons.user)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key('Autocomplete')), 'Doe'); + await tester.enterText( + find.byKey(const Key('AutocompleteLecturer')), 'Doe'); await tester.pumpAndSettle(); expect(find.text('Jane Doe'), findsOneWidget); @@ -1167,14 +1169,15 @@ Future main() async { // Select lecturer await tester.tap(find.text('Lecturer')); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key('Autocomplete')), 'Doe'); + await tester.enterText( + find.byKey(const Key('AutocompleteLecturer')), 'Doe'); await tester.pumpAndSettle(); expect(find.text('Jane Doe'), findsOneWidget); expect(find.text('John Doe'), findsOneWidget); await tester.enterText( - find.byKey(const Key('Autocomplete')), 'John Doe'); + find.byKey(const Key('AutocompleteLecturer')), 'John Doe'); await tester.pumpAndSettle(); FocusManager.instance.primaryFocus.unfocus(); From 52a7d5ca6d3b2cc23b3ad20c8fc14bc6d9ef650f Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Mon, 10 May 2021 23:29:32 +0300 Subject: [PATCH 20/59] Initiate creation of separate widgets --- .../model/questions/question.dart | 11 ++ .../service/feedback_provider.dart | 29 ++++ .../view/class_feedback_view.dart | 152 ++++++++++-------- 3 files changed, 125 insertions(+), 67 deletions(-) create mode 100644 lib/pages/class_feedback/model/questions/question.dart diff --git a/lib/pages/class_feedback/model/questions/question.dart b/lib/pages/class_feedback/model/questions/question.dart new file mode 100644 index 000000000..aa647bd39 --- /dev/null +++ b/lib/pages/class_feedback/model/questions/question.dart @@ -0,0 +1,11 @@ +class FeedbackQuestion { + const FeedbackQuestion({ + this.question, + this.category, + this.type, + }); + + final String question; + final String category; + final String type; +} diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index c506d893d..2dcd83b2e 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -20,6 +20,20 @@ extension ClassFeedbackAnswerExtension on ClassFeedbackAnswer { } } +// extension FeedbackQuestionExtension on FeedbackQuestion { +// static FeedbackQuestion fromJSON(Map json) { +// if (json['category'] == null || +// json['question'] == null || +// json['type'] == null) { +// return null; +// } +// +// if (json['type'] == 'input') { +// return +// } else if (json['type'] == 'rating') {} +// } +// } + class FeedbackProvider with ChangeNotifier { Future addResponse(ClassFeedbackAnswer response) async { try { @@ -51,6 +65,21 @@ class FeedbackProvider with ChangeNotifier { } } + Future> fetchCategories() async { + try { + final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance + .collection('forms') + .doc('class_feedback_questions') + .get(); + final Map data = documentSnapshot['categories']; + return data; + } catch (e) { + print(e); + AppToast.show(S.current.errorSomethingWentWrong); + return null; + } + } + List getQuestionsByCategoryAndType( List questions, String category, String type) { final List filteredQuestions = []; diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 14c7d3d17..be6e553a0 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -33,6 +33,7 @@ class _ClassFeedbackViewState extends State { Person selectedAssistant; List classTeachers = []; Map feedbackQuestions = {}; + Map feedbackCategories = {}; Map responses = {}; List> initialValues = []; @@ -53,10 +54,14 @@ class _ClassFeedbackViewState extends State { .fetchPeople() .then((teachers) => setState(() => classTeachers = teachers)); - awaitFeedbackQuestions(); + Provider.of(context, listen: false) + .fetchCategories() + .then((categories) => setState(() => feedbackCategories = categories)); + + fetchFeedbackQuestions(); } - Future> awaitFeedbackQuestions() async { + Future> fetchFeedbackQuestions() async { await Provider.of(context, listen: false) .fetchQuestions() .then((questions) => setState(() => feedbackQuestions = questions)); @@ -74,9 +79,82 @@ class _ClassFeedbackViewState extends State { return feedbackQuestions; } + Widget classWidget() { + return TextFormField( + enabled: false, + controller: classController, + decoration: InputDecoration( + labelText: S.of(context).labelClass, + prefixIcon: const Icon(FeatherIcons.bookOpen), + ), + onChanged: (_) => setState(() {}), + ); + } + + Widget lecturerWidget(BuildContext context) { + final personProvider = Provider.of(context); + + return FutureBuilder( + future: personProvider.mostRecentLecturer(widget.classHeader.id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + final lecturerName = snapshot.data; + selectedTeacherName = lecturerName; + return TextFormField( + enabled: false, + controller: TextEditingController(text: lecturerName ?? '-'), + decoration: InputDecoration( + labelText: S.of(context).labelLecturer, + prefixIcon: const Icon(Icons.person_outline), + ), + onChanged: (_) => setState(() {}), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + } + + Widget assistantWidget() { + return AutocompletePerson( + key: const Key('AutocompleteAssistant'), + labelText: S.current.labelAssistant, + warning: S.current.warningYouNeedToSelectAssistant, + formKey: formKey, + onSaved: (value) => selectedAssistant = value, + classTeachers: classTeachers, + ); + } + + Widget acknowledgementWidget() { + return Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: agreedToResponsibilities, + visualDensity: VisualDensity.compact, + onChanged: (value) => + setState(() => agreedToResponsibilities = value), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10.25), + child: Text( + S.of(context).messageAgreeFeedbackPolicy, + style: Theme.of(context).textTheme.subtitle1, + ), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { - final personProvider = Provider.of(context); final feedbackProvider = Provider.of(context); final List generalQuestionsRating = @@ -135,70 +213,10 @@ class _ClassFeedbackViewState extends State { icon: Icons.info_outline, text: S.of(context).infoFormAnonymous, ), - TextFormField( - enabled: false, - controller: classController, - decoration: InputDecoration( - labelText: S.of(context).labelClass, - prefixIcon: const Icon(FeatherIcons.bookOpen), - ), - onChanged: (_) => setState(() {}), - ), - FutureBuilder( - future: personProvider - .mostRecentLecturer(widget.classHeader.id), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done) { - final lecturerName = snapshot.data; - selectedTeacherName = lecturerName; - return TextFormField( - enabled: false, - controller: TextEditingController( - text: lecturerName ?? '-'), - decoration: InputDecoration( - labelText: S.of(context).labelLecturer, - prefixIcon: const Icon(Icons.person_outline), - ), - onChanged: (_) => setState(() {}), - ); - } else { - return const Center( - child: CircularProgressIndicator()); - } - }, - ), - AutocompletePerson( - key: const Key('AutocompleteAssistant'), - labelText: S.current.labelAssistant, - warning: S.current.warningYouNeedToSelectAssistant, - formKey: formKey, - onSaved: (value) => selectedAssistant = value, - classTeachers: classTeachers, - ), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Checkbox( - value: agreedToResponsibilities, - visualDensity: VisualDensity.compact, - onChanged: (value) => setState( - () => agreedToResponsibilities = value), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 10.25), - child: Text( - S.of(context).messageAgreeFeedbackPolicy, - style: Theme.of(context).textTheme.subtitle1, - ), - ), - ), - ], - ), - ), + classWidget(), + lecturerWidget(context), + assistantWidget(), + acknowledgementWidget(), const SizedBox(height: 24), Card( child: Padding( From 6e57a85377868d0d0f5c4e74a718b3e19565fa20 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 12 May 2021 00:20:59 +0300 Subject: [PATCH 21/59] Improve questions instantiation logic --- lib/generated/intl/messages_en.dart | 4 - lib/generated/intl/messages_ro.dart | 4 - lib/generated/l10n.dart | 40 -- lib/l10n/intl_en.arb | 4 - lib/l10n/intl_ro.arb | 4 - .../model/questions/question.dart | 13 + .../model/questions/question_dropdown.dart | 14 + .../service/feedback_provider.dart | 47 +- .../view/class_feedback_view.dart | 612 +++++------------- 9 files changed, 233 insertions(+), 509 deletions(-) create mode 100644 lib/pages/class_feedback/model/questions/question_dropdown.dart diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index f8e8b64cd..ea3418137 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -238,15 +238,11 @@ class MessageLookup extends MessageLookupByLibrary { "navigationTimetable" : MessageLookupByLibrary.simpleMessage("Timetable"), "relevanceAnyone" : MessageLookupByLibrary.simpleMessage("Anyone"), "relevanceOnlyMe" : MessageLookupByLibrary.simpleMessage("Only me"), - "sectionApplications" : MessageLookupByLibrary.simpleMessage("Applications"), "sectionEvents" : MessageLookupByLibrary.simpleMessage("Events"), "sectionEventsComingUp" : MessageLookupByLibrary.simpleMessage("Events coming up"), "sectionFAQ" : MessageLookupByLibrary.simpleMessage("FAQ"), "sectionFrequentlyAccessedWebsites" : MessageLookupByLibrary.simpleMessage("Favourite websites"), - "sectionGeneralQuestions" : MessageLookupByLibrary.simpleMessage("General questions"), "sectionGrading" : MessageLookupByLibrary.simpleMessage("Grading"), - "sectionInvolvement" : MessageLookupByLibrary.simpleMessage("Involvement"), - "sectionPersonalComments" : MessageLookupByLibrary.simpleMessage("Personal comments"), "sectionShortcuts" : MessageLookupByLibrary.simpleMessage("Shortcuts"), "settingsExportToGoogleCalendar" : MessageLookupByLibrary.simpleMessage("Export events to Google Calendar"), "settingsItemDarkMode" : MessageLookupByLibrary.simpleMessage("Dark Mode"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 4ccd7bfda..b23d99b0a 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -238,15 +238,11 @@ class MessageLookup extends MessageLookupByLibrary { "navigationTimetable" : MessageLookupByLibrary.simpleMessage("Orar"), "relevanceAnyone" : MessageLookupByLibrary.simpleMessage("Oricine"), "relevanceOnlyMe" : MessageLookupByLibrary.simpleMessage("Doar eu"), - "sectionApplications" : MessageLookupByLibrary.simpleMessage("Aplicații"), "sectionEvents" : MessageLookupByLibrary.simpleMessage("Evenimente"), "sectionEventsComingUp" : MessageLookupByLibrary.simpleMessage("Evenimente următoare"), "sectionFAQ" : MessageLookupByLibrary.simpleMessage("Întrebări frecvente"), "sectionFrequentlyAccessedWebsites" : MessageLookupByLibrary.simpleMessage("Website-uri favorite"), - "sectionGeneralQuestions" : MessageLookupByLibrary.simpleMessage("Întrebări generale"), "sectionGrading" : MessageLookupByLibrary.simpleMessage("Punctaj"), - "sectionInvolvement" : MessageLookupByLibrary.simpleMessage("Implicare"), - "sectionPersonalComments" : MessageLookupByLibrary.simpleMessage("Comentarii personale"), "sectionShortcuts" : MessageLookupByLibrary.simpleMessage("Scurtături"), "settingsExportToGoogleCalendar" : MessageLookupByLibrary.simpleMessage("Exportă evenimentele în Google Calendar"), "settingsItemDarkMode" : MessageLookupByLibrary.simpleMessage("Mod Întunecat"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 17a936bce..95bb93dac 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -605,46 +605,6 @@ class S { ); } - /// `General questions` - String get sectionGeneralQuestions { - return Intl.message( - 'General questions', - name: 'sectionGeneralQuestions', - desc: '', - args: [], - ); - } - - /// `Personal comments` - String get sectionPersonalComments { - return Intl.message( - 'Personal comments', - name: 'sectionPersonalComments', - desc: '', - args: [], - ); - } - - /// `Applications` - String get sectionApplications { - return Intl.message( - 'Applications', - name: 'sectionApplications', - desc: '', - args: [], - ); - } - - /// `Involvement` - String get sectionInvolvement { - return Intl.message( - 'Involvement', - name: 'sectionInvolvement', - desc: '', - args: [], - ); - } - /// `Main page` String get shortcutTypeMain { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6df1833f0..3b4ff3634 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -58,10 +58,6 @@ "sectionEventsComingUp": "Events coming up", "sectionFAQ": "FAQ", "sectionGrading": "Grading", - "sectionGeneralQuestions": "General questions", - "sectionPersonalComments": "Personal comments", - "sectionApplications": "Applications", - "sectionInvolvement": "Involvement", "shortcutTypeMain": "Main page", "shortcutTypeClassbook": "Classbook", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 150755fa7..6b07c2869 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -58,10 +58,6 @@ "sectionEventsComingUp": "Evenimente următoare", "sectionFAQ": "Întrebări frecvente", "sectionGrading": "Punctaj", - "sectionGeneralQuestions": "Întrebări generale", - "sectionPersonalComments": "Comentarii personale", - "sectionApplications": "Aplicații", - "sectionInvolvement": "Implicare", "shortcutTypeMain": "Pagina principală", "shortcutTypeClassbook": "Catalog", diff --git a/lib/pages/class_feedback/model/questions/question.dart b/lib/pages/class_feedback/model/questions/question.dart index aa647bd39..2fa92cdcc 100644 --- a/lib/pages/class_feedback/model/questions/question.dart +++ b/lib/pages/class_feedback/model/questions/question.dart @@ -3,9 +3,22 @@ class FeedbackQuestion { this.question, this.category, this.type, + this.id, }); final String question; final String category; final String type; + final String id; + + @override + int get hashCode => question.hashCode; + + @override + bool operator ==(Object other) { + if (other is FeedbackQuestion) { + return other.question == question; + } + return false; + } } diff --git a/lib/pages/class_feedback/model/questions/question_dropdown.dart b/lib/pages/class_feedback/model/questions/question_dropdown.dart new file mode 100644 index 000000000..7b37a8227 --- /dev/null +++ b/lib/pages/class_feedback/model/questions/question_dropdown.dart @@ -0,0 +1,14 @@ +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; + +class FeedbackQuestionDropdown extends FeedbackQuestion { + FeedbackQuestionDropdown({ + String question, + String category, + String type, + String id, + List answerOptions, + }) : options = answerOptions, + super(question: question, category: category, type: type, id: id); + + List options; +} diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 2dcd83b2e..4e21b6843 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -1,4 +1,6 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; @@ -20,19 +22,29 @@ extension ClassFeedbackAnswerExtension on ClassFeedbackAnswer { } } -// extension FeedbackQuestionExtension on FeedbackQuestion { -// static FeedbackQuestion fromJSON(Map json) { -// if (json['category'] == null || -// json['question'] == null || -// json['type'] == null) { -// return null; -// } -// -// if (json['type'] == 'input') { -// return -// } else if (json['type'] == 'rating') {} -// } -// } +extension FeedbackQuestionExtension on FeedbackQuestion { + static FeedbackQuestion fromJSON(dynamic json, String id) { + if (json['type'] == 'dropdown' && json['options'] != null) { + final List options = json['options']; + final List optionsString = + options.map((e) => e as String).toList(); + return FeedbackQuestionDropdown( + category: json['category'], + question: json['question'][LocaleProvider.localeString], + type: json['type'], + id: id, + answerOptions: optionsString, + ); + } else { + return FeedbackQuestion( + category: json['category'], + question: json['question'][LocaleProvider.localeString], + type: json['type'], + id: id, + ); + } + } +} class FeedbackProvider with ChangeNotifier { Future addResponse(ClassFeedbackAnswer response) async { @@ -50,14 +62,19 @@ class FeedbackProvider with ChangeNotifier { } } - Future> fetchQuestions() async { + Future> fetchQuestions() async { try { final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance .collection('forms') .doc('class_feedback_questions') .get(); final Map data = documentSnapshot['questions']; - return data; + final Map questions = {}; + for (final value in data.values) { + final key = data.keys.firstWhere((element) => data[element] == value); + questions[key] = FeedbackQuestionExtension.fromJSON(value, key); + } + return questions; } catch (e) { print(e); AppToast.show(S.current.errorSomethingWentWrong); diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index be6e553a0..1c8cb8aa1 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -1,9 +1,12 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; +import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; @@ -28,14 +31,13 @@ class _ClassFeedbackViewState extends State { TextEditingController classController; bool agreedToResponsibilities; - List involvementPercentages = []; String selectedTeacherName; Person selectedAssistant; List classTeachers = []; - Map feedbackQuestions = {}; Map feedbackCategories = {}; Map responses = {}; List> initialValues = []; + Map feedbackQuestions = {}; @override void initState() { @@ -43,13 +45,7 @@ class _ClassFeedbackViewState extends State { agreedToResponsibilities = false; classController = TextEditingController(text: widget.classHeader?.id ?? ''); - involvementPercentages = [ - '0% ... 20%', - '20% ... 40%', - '40% ... 60%', - '60% ... 80%', - '80% ... 100%' - ]; + Provider.of(context, listen: false) .fetchPeople() .then((teachers) => setState(() => classTeachers = teachers)); @@ -153,47 +149,174 @@ class _ClassFeedbackViewState extends State { ); } - @override - Widget build(BuildContext context) { - final feedbackProvider = Provider.of(context); - - final List generalQuestionsRating = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'general', 'rating'); - - final List generalQuestionsRatingIndexes = feedbackQuestions.keys - .where((element) => - feedbackQuestions[element]['type'] == 'rating' && - feedbackQuestions[element]['category'] == 'general') - .toList(); - - final List generalQuestionsInput = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'general', 'input'); - - final List involvementQuestions = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'involvement', 'input'); - - final List lectureQuestions = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'lecture', 'rating'); - - final List applicationsQuestions = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'applications', 'rating'); + Widget categoryHeader(String category) { + return Column( + children: [ + Text( + category, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 24), + ], + ); + } - final List homeworkQuestionsRating = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'homework', 'rating'); + Widget questionWidget(FeedbackQuestion question) { + if (question.type == 'input') { + return Column( + children: [ + Text( + question.question, + style: const TextStyle( + fontSize: 18, + ), + ), + TextFormField( + onSaved: (value) { + responses[int.parse(question.id)] = value; + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value?.isEmpty ?? true) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 10), + ], + ); + } else if (question.type == 'rating') { + return Column( + children: [ + EmojiFormField( + question: question.question, + onSaved: (value) { + responses[int.parse(question.id)] = value.keys + .firstWhere((element) => value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values.where((e) => e != false).isEmpty) { + return S.of(context).warningYouNeedToSelectAtLeastOne; + } + return null; + }, + initialValues: initialValues[int.parse(question.id)], + ), + const SizedBox(height: 10), + ], + ); + } else if (question.type == 'dropdown') { + return Column( + children: [ + Text( + question.question, + style: const TextStyle( + fontSize: 18, + ), + ), + DropdownButtonFormField( + onSaved: (value) { + responses[int.parse(question.id)] = value; + }, + items: (question as FeedbackQuestionDropdown) + .options + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => {}); + }, + validator: (selection) { + if (selection == null) { + return S.of(context).errorEventTypeCannotBeEmpty; + } + return null; + }, + ), + const SizedBox(height: 10), + ], + ); + } else if (question.type == 'text') { + return Column( + children: [ + Text( + question.question, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + onSaved: (value) { + responses[int.parse(question.id)] = value; + }, + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + ], + ); + } else { + return null; + } + } - final List homeworkQuestionsInput = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'homework', 'input'); + @override + Widget build(BuildContext context) { + final List children = [ + IconText( + icon: Icons.info_outline, + text: S.of(context).infoFormAnonymous, + ), + classWidget(), + lecturerWidget(context), + assistantWidget(), + acknowledgementWidget(), + const SizedBox(height: 24), + ]; - final List personalQuestions = - feedbackProvider.getQuestionsByCategoryAndType( - feedbackQuestions.values.toList(), 'personal', 'input'); + for (final category in feedbackCategories.keys) { + final List categoryChildren = [ + categoryHeader( + feedbackCategories[category][LocaleProvider.localeString]) + ]; + for (final question + in feedbackQuestions.values.where((q) => q.category == category)) { + categoryChildren.add(questionWidget(question)); + } + children.add( + Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: categoryChildren, + ), + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } return AppScaffold( title: Text(S.of(context).navigationClassFeedback), @@ -204,395 +327,7 @@ class _ClassFeedbackViewState extends State { padding: const EdgeInsets.all(10), child: Form( key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - children: [ - IconText( - icon: Icons.info_outline, - text: S.of(context).infoFormAnonymous, - ), - classWidget(), - lecturerWidget(context), - assistantWidget(), - acknowledgementWidget(), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionGeneralQuestions, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - generalQuestionsInput.isNotEmpty - ? generalQuestionsInput.single - : '-', - style: const TextStyle( - fontSize: 18, - ), - ), - TextFormField( - onSaved: (value) { - responses[int.parse( - feedbackQuestions.keys.isNotEmpty - ? feedbackQuestions.keys.firstWhere( - (element) => - feedbackQuestions[element] - ['type'] == - 'input' && - feedbackQuestions[element] - ['category'] == - 'general') - : '-1')] = value; - }, - autovalidateMode: - AutovalidateMode.onUserInteraction, - decoration: InputDecoration( - labelText: S.of(context).labelGrade, - prefixIcon: const Icon(Icons.grade_outlined), - ), - validator: (value) { - if (value?.isEmpty ?? true) { - return S.current - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 24), - ...generalQuestionsRating.asMap().entries.map( - (entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[int.parse( - generalQuestionsRatingIndexes[ - entry.key])] = value.keys - .firstWhere((element) => - value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: initialValues[entry.key], - ); - }, - ), - ], - ), - ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionInvolvement, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - involvementQuestions.isNotEmpty - ? involvementQuestions.single - : '-', - style: const TextStyle( - fontSize: 18, - ), - ), - DropdownButtonFormField( - decoration: InputDecoration( - labelText: S.of(context).sectionInvolvement, - prefixIcon: - const Icon(Icons.local_activity_outlined), - ), - onSaved: (value) { - responses[generalQuestionsInput.length + - generalQuestionsRating.length] = value; - }, - items: involvementPercentages - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type.toString()), - ), - ) - .toList(), - onChanged: (selection) { - formKey.currentState.validate(); - setState(() => {}); - }, - validator: (selection) { - if (selection == null) { - return S - .of(context) - .errorEventTypeCannotBeEmpty; - } - return null; - }, - ), - ], - ), - ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).uniEventTypeLecture, - style: Theme.of(context).textTheme.headline6, - ), - ...lectureQuestions.asMap().entries.map( - (entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - entry.key] = - value.keys - .firstWhere((element) => - value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: initialValues[ - generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - entry.key], - ); - }, - ), - ], - ), - ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionApplications, - style: Theme.of(context).textTheme.headline6, - ), - ...applicationsQuestions.asMap().entries.map( - (entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - lectureQuestions.length + - entry.key] = - value.keys - .firstWhere((element) => - value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: initialValues[ - generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - lectureQuestions.length + - entry.key], - ); - }, - ), - ], - ), - ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).uniEventTypeHomework, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - Text( - homeworkQuestionsInput.isNotEmpty - ? homeworkQuestionsInput.single - : '-', - style: const TextStyle( - fontSize: 18, - ), - ), - TextFormField( - onSaved: (value) { - responses[generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - lectureQuestions.length + - applicationsQuestions.length] = value; - }, - autovalidateMode: - AutovalidateMode.onUserInteraction, - decoration: InputDecoration( - labelText: S.current.labelGrade, - prefixIcon: const Icon(Icons.grade_outlined), - ), - validator: (value) { - if (value?.isEmpty ?? true) { - return S.current - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - onChanged: (_) => setState(() {}), - ), - ...homeworkQuestionsRating.asMap().entries.map( - (entry) { - return EmojiFormField( - question: entry.value, - onSaved: (value) { - responses[generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - lectureQuestions.length + - applicationsQuestions.length + - homeworkQuestionsInput.length + - entry.key] = - value.keys - .firstWhere((element) => - value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, - initialValues: initialValues[ - generalQuestionsInput.length + - generalQuestionsRating.length + - involvementQuestions.length + - lectureQuestions.length + - applicationsQuestions.length + - homeworkQuestionsInput.length + - entry.key], - ); - }, - ), - ], - ), - ), - ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - Text( - S.of(context).sectionPersonalComments, - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 24), - ...personalQuestions.asMap().entries.map( - (entry) { - return Column( - children: [ - Text( - entry.value, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - onSaved: (value) { - responses[generalQuestionsInput - .length + - generalQuestionsRating - .length + - involvementQuestions - .length + - lectureQuestions.length + - applicationsQuestions - .length + - homeworkQuestionsInput - .length + - homeworkQuestionsRating - .length + - entry.key] = value; - }, - keyboardType: - TextInputType.multiline, - maxLines: null, - ), - ], - ), - ), - ), - const SizedBox(height: 24), - ], - ); - }, - ), - ], - ), - ), - ), - ], - ), - ], - ), + child: Column(mainAxisSize: MainAxisSize.min, children: children), ), ), ], @@ -604,7 +339,6 @@ class _ClassFeedbackViewState extends State { text: S.current.buttonSend, onPressed: () async { if (!formKey.currentState.validate()) return; - if (!agreedToResponsibilities) { AppToast.show( '${S.current.warningAgreeTo}${S.current.labelFeedbackPolicy}.'); @@ -619,7 +353,9 @@ class _ClassFeedbackViewState extends State { for (var i = 0; i < feedbackQuestions.length; i++) { res = false; - if (feedbackQuestions[i.toString()]['type'] == 'input') { + if (feedbackQuestions[i.toString()].type == 'input' || + feedbackQuestions[i.toString()].type == 'text' || + feedbackQuestions[i.toString()].type == 'dropdown') { final response = ClassFeedbackAnswer( assistant: selectedAssistant, teacherName: selectedTeacherName, From 0bab65e7e84241e784d1e225e439b24f58c0b639 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 12 May 2021 00:36:54 +0300 Subject: [PATCH 22/59] Remove S.of(context) --- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ro.dart | 1 + lib/generated/l10n.dart | 10 ++++++++++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ro.arb | 1 + .../class_feedback/view/class_feedback_view.dart | 16 ++++++++-------- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index ea3418137..ed6b47742 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -85,6 +85,7 @@ class MessageLookup extends MessageLookupByLibrary { "buttonSend" : MessageLookupByLibrary.simpleMessage("Send"), "buttonSet" : MessageLookupByLibrary.simpleMessage("Set"), "errorAccountDisabled" : MessageLookupByLibrary.simpleMessage("The account has been disabled."), + "errorAnswerCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Answer cannot be empty."), "errorClassCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Class cannot be empty."), "errorCouldNotLaunchURL" : m1, "errorEmailInUse" : MessageLookupByLibrary.simpleMessage("There is already an account associated with this e-mail address"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index b23d99b0a..567910cf1 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -85,6 +85,7 @@ class MessageLookup extends MessageLookupByLibrary { "buttonSend" : MessageLookupByLibrary.simpleMessage("Trimitere"), "buttonSet" : MessageLookupByLibrary.simpleMessage("Setează"), "errorAccountDisabled" : MessageLookupByLibrary.simpleMessage("Contul a fost dezactivat."), + "errorAnswerCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Răspunsul trebuie precizat."), "errorClassCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Materia trebuie precizată."), "errorCouldNotLaunchURL" : m1, "errorEmailInUse" : MessageLookupByLibrary.simpleMessage("Există deja un cont asociat acestui e-mail."), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 95bb93dac..b33a5ef4a 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1195,6 +1195,16 @@ class S { ); } + /// `Answer cannot be empty.` + String get errorAnswerCannotBeEmpty { + return Intl.message( + 'Answer cannot be empty.', + name: 'errorAnswerCannotBeEmpty', + desc: '', + args: [], + ); + } + /// `Please select a picture that is less than 5MB.` String get errorPictureSizeToBig { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3b4ff3634..f3f014d51 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -121,6 +121,7 @@ "errorPermissionDenied": "You do not have permission to do that.", "errorEventTypeCannotBeEmpty": "Event type cannot be empty.", "errorClassCannotBeEmpty": "Class cannot be empty.", + "errorAnswerCannotBeEmpty": "Answer cannot be empty.", "errorPictureSizeToBig": "Please select a picture that is less than 5MB.", "errorImage": "The image could not be loaded.", "errorInsertGoogleEvents": "Unable to insert events in Google Calendar.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 6b07c2869..755428125 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -121,6 +121,7 @@ "errorPermissionDenied": "Nu aveți suficiente permisiuni.", "errorEventTypeCannotBeEmpty": "Tipul de eveniment trebuie precizat.", "errorClassCannotBeEmpty": "Materia trebuie precizată.", + "errorAnswerCannotBeEmpty": "Răspunsul trebuie precizat.", "errorPictureSizeToBig": "Selectați o fotografie care are mai puțin de 5MB.", "errorImage": "Imaginea nu putut fi încărcată.", "errorInsertGoogleEvents": "Evenimentele nu au putut fi inserate în Google Calendar.", diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 1c8cb8aa1..f7746fef5 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -80,7 +80,7 @@ class _ClassFeedbackViewState extends State { enabled: false, controller: classController, decoration: InputDecoration( - labelText: S.of(context).labelClass, + labelText: S.current.labelClass, prefixIcon: const Icon(FeatherIcons.bookOpen), ), onChanged: (_) => setState(() {}), @@ -100,7 +100,7 @@ class _ClassFeedbackViewState extends State { enabled: false, controller: TextEditingController(text: lecturerName ?? '-'), decoration: InputDecoration( - labelText: S.of(context).labelLecturer, + labelText: S.current.labelLecturer, prefixIcon: const Icon(Icons.person_outline), ), onChanged: (_) => setState(() {}), @@ -139,7 +139,7 @@ class _ClassFeedbackViewState extends State { child: Padding( padding: const EdgeInsets.only(top: 10.25), child: Text( - S.of(context).messageAgreeFeedbackPolicy, + S.current.messageAgreeFeedbackPolicy, style: Theme.of(context).textTheme.subtitle1, ), ), @@ -178,7 +178,7 @@ class _ClassFeedbackViewState extends State { autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { if (value?.isEmpty ?? true) { - return S.current.warningYouNeedToSelectAtLeastOne; + return S.current.errorAnswerCannotBeEmpty; } return null; }, @@ -199,7 +199,7 @@ class _ClassFeedbackViewState extends State { }, validator: (selection) { if (selection.values.where((e) => e != false).isEmpty) { - return S.of(context).warningYouNeedToSelectAtLeastOne; + return S.current.warningYouNeedToSelectAtLeastOne; } return null; }, @@ -236,7 +236,7 @@ class _ClassFeedbackViewState extends State { }, validator: (selection) { if (selection == null) { - return S.of(context).errorEventTypeCannotBeEmpty; + return S.current.errorAnswerCannotBeEmpty; } return null; }, @@ -283,7 +283,7 @@ class _ClassFeedbackViewState extends State { final List children = [ IconText( icon: Icons.info_outline, - text: S.of(context).infoFormAnonymous, + text: S.current.infoFormAnonymous, ), classWidget(), lecturerWidget(context), @@ -319,7 +319,7 @@ class _ClassFeedbackViewState extends State { } return AppScaffold( - title: Text(S.of(context).navigationClassFeedback), + title: Text(S.current.navigationClassFeedback), actions: [_submitButton()], body: ListView( children: [ From c1daa63a5d5e3f0d8b44d587127b1f85a257f45d Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 12 May 2021 01:17:30 +0300 Subject: [PATCH 23/59] Add input questions' validator --- lib/generated/intl/messages_en.dart | 2 ++ lib/generated/intl/messages_ro.dart | 2 ++ lib/generated/l10n.dart | 20 +++++++++++++++++++ lib/l10n/intl_en.arb | 2 ++ lib/l10n/intl_ro.arb | 2 ++ .../view/class_feedback_view.dart | 14 +++++++++++++ 6 files changed, 42 insertions(+) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index ed6b47742..cc71ff5e8 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -86,6 +86,7 @@ class MessageLookup extends MessageLookupByLibrary { "buttonSet" : MessageLookupByLibrary.simpleMessage("Set"), "errorAccountDisabled" : MessageLookupByLibrary.simpleMessage("The account has been disabled."), "errorAnswerCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Answer cannot be empty."), + "errorAnswerIncorrect" : MessageLookupByLibrary.simpleMessage("The answer you entered is incorrect."), "errorClassCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Class cannot be empty."), "errorCouldNotLaunchURL" : m1, "errorEmailInUse" : MessageLookupByLibrary.simpleMessage("There is already an account associated with this e-mail address"), @@ -133,6 +134,7 @@ class MessageLookup extends MessageLookupByLibrary { "infoRelevanceNothingSelected" : MessageLookupByLibrary.simpleMessage("If this is relevant for everyone, don\'t select anything ."), "infoSelect" : MessageLookupByLibrary.simpleMessage("Select the"), "infoYouNeedToSelect" : MessageLookupByLibrary.simpleMessage("You first need to select the"), + "labelAnswer" : MessageLookupByLibrary.simpleMessage("Answer"), "labelAskPermissions" : MessageLookupByLibrary.simpleMessage("Request editing permissions"), "labelAssistant" : MessageLookupByLibrary.simpleMessage("Assistant"), "labelCategory" : MessageLookupByLibrary.simpleMessage("Category"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 567910cf1..fa397cdc3 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -86,6 +86,7 @@ class MessageLookup extends MessageLookupByLibrary { "buttonSet" : MessageLookupByLibrary.simpleMessage("Setează"), "errorAccountDisabled" : MessageLookupByLibrary.simpleMessage("Contul a fost dezactivat."), "errorAnswerCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Răspunsul trebuie precizat."), + "errorAnswerIncorrect" : MessageLookupByLibrary.simpleMessage("Răspunsul introdus nu este corect."), "errorClassCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Materia trebuie precizată."), "errorCouldNotLaunchURL" : m1, "errorEmailInUse" : MessageLookupByLibrary.simpleMessage("Există deja un cont asociat acestui e-mail."), @@ -133,6 +134,7 @@ class MessageLookup extends MessageLookupByLibrary { "infoRelevanceNothingSelected" : MessageLookupByLibrary.simpleMessage("Nu selectați nimic dacă este relevant pentru toată lumea."), "infoSelect" : MessageLookupByLibrary.simpleMessage("Selectați"), "infoYouNeedToSelect" : MessageLookupByLibrary.simpleMessage("Trebuie întâi să selectați"), + "labelAnswer" : MessageLookupByLibrary.simpleMessage("Răspuns"), "labelAskPermissions" : MessageLookupByLibrary.simpleMessage("Cere permisiuni de editare"), "labelAssistant" : MessageLookupByLibrary.simpleMessage("Asistent"), "labelCategory" : MessageLookupByLibrary.simpleMessage("Categorie"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index b33a5ef4a..3011c6a8f 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -165,6 +165,16 @@ class S { ); } + /// `Answer` + String get labelAnswer { + return Intl.message( + 'Answer', + name: 'labelAnswer', + desc: '', + args: [], + ); + } + /// `Version` String get labelVersion { return Intl.message( @@ -1205,6 +1215,16 @@ class S { ); } + /// `The answer you entered is incorrect.` + String get errorAnswerIncorrect { + return Intl.message( + 'The answer you entered is incorrect.', + name: 'errorAnswerIncorrect', + desc: '', + args: [], + ); + } + /// `Please select a picture that is less than 5MB.` String get errorPictureSizeToBig { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f3f014d51..658dfb0ad 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -13,6 +13,7 @@ "labelConfirmPassword": "Confirm password", "labelConfirmNewPassword": "Confirm new password", "labelAskPermissions": "Request editing permissions", + "labelAnswer": "Answer", "labelVersion": "Version", "labelFirstName": "First name", "labelLastName": "Last name", @@ -122,6 +123,7 @@ "errorEventTypeCannotBeEmpty": "Event type cannot be empty.", "errorClassCannotBeEmpty": "Class cannot be empty.", "errorAnswerCannotBeEmpty": "Answer cannot be empty.", + "errorAnswerIncorrect": "The answer you entered is incorrect.", "errorPictureSizeToBig": "Please select a picture that is less than 5MB.", "errorImage": "The image could not be loaded.", "errorInsertGoogleEvents": "Unable to insert events in Google Calendar.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 755428125..ba8e5ca3b 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -13,6 +13,7 @@ "labelConfirmPassword": "Confirmare parolă", "labelConfirmNewPassword": "Confirmare parolă nouă", "labelAskPermissions": "Cere permisiuni de editare", + "labelAnswer": "Răspuns", "labelVersion": "Versiunea", "labelFirstName": "Prenume", "labelLastName": "Nume", @@ -122,6 +123,7 @@ "errorEventTypeCannotBeEmpty": "Tipul de eveniment trebuie precizat.", "errorClassCannotBeEmpty": "Materia trebuie precizată.", "errorAnswerCannotBeEmpty": "Răspunsul trebuie precizat.", + "errorAnswerIncorrect": "Răspunsul introdus nu este corect.", "errorPictureSizeToBig": "Selectați o fotografie care are mai puțin de 5MB.", "errorImage": "Imaginea nu putut fi încărcată.", "errorInsertGoogleEvents": "Evenimentele nu au putut fi inserate în Google Calendar.", diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index f7746fef5..cde53b216 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:validators/validators.dart'; class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -172,6 +173,10 @@ class _ClassFeedbackViewState extends State { ), ), TextFormField( + decoration: InputDecoration( + labelText: S.current.labelAnswer, + prefixIcon: const Icon(Icons.question_answer_outlined), + ), onSaved: (value) { responses[int.parse(question.id)] = value; }, @@ -180,6 +185,11 @@ class _ClassFeedbackViewState extends State { if (value?.isEmpty ?? true) { return S.current.errorAnswerCannotBeEmpty; } + if (!isNumeric(value) || + int.parse(value) < 0 || + int.parse(value) > 10) { + return S.current.errorAnswerIncorrect; + } return null; }, onChanged: (_) => setState(() {}), @@ -218,6 +228,10 @@ class _ClassFeedbackViewState extends State { ), ), DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelAnswer, + prefixIcon: const Icon(Icons.list_outlined), + ), onSaved: (value) { responses[int.parse(question.id)] = value; }, From 31e74d8d0291b1ab77d5826b7d9e60fa9490ffa4 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 12 May 2021 14:49:38 +0300 Subject: [PATCH 24/59] Add input questions' validator --- .../view/class_feedback_view.dart | 26 +++++++++---------- lib/widgets/radio_emoji.dart | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index cde53b216..57760243c 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -37,7 +37,7 @@ class _ClassFeedbackViewState extends State { List classTeachers = []; Map feedbackCategories = {}; Map responses = {}; - List> initialValues = []; + List> answerValues = []; Map feedbackQuestions = {}; @override @@ -64,7 +64,7 @@ class _ClassFeedbackViewState extends State { .then((questions) => setState(() => feedbackQuestions = questions)); for (int i = 0; i <= feedbackQuestions.length; i++) { - initialValues.insert(i, { + answerValues.insert(i, { 0: false, 1: false, 2: false, @@ -76,7 +76,7 @@ class _ClassFeedbackViewState extends State { return feedbackQuestions; } - Widget classWidget() { + Widget classFormField() { return TextFormField( enabled: false, controller: classController, @@ -88,7 +88,7 @@ class _ClassFeedbackViewState extends State { ); } - Widget lecturerWidget(BuildContext context) { + Widget lecturerFormField(BuildContext context) { final personProvider = Provider.of(context); return FutureBuilder( @@ -113,7 +113,7 @@ class _ClassFeedbackViewState extends State { ); } - Widget assistantWidget() { + Widget assistantFormField() { return AutocompletePerson( key: const Key('AutocompleteAssistant'), labelText: S.current.labelAssistant, @@ -124,7 +124,7 @@ class _ClassFeedbackViewState extends State { ); } - Widget acknowledgementWidget() { + Widget acknowledgementFormField() { return Padding( padding: const EdgeInsets.all(10), child: Row( @@ -162,7 +162,7 @@ class _ClassFeedbackViewState extends State { ); } - Widget questionWidget(FeedbackQuestion question) { + Widget questionFormField(FeedbackQuestion question) { if (question.type == 'input') { return Column( children: [ @@ -213,7 +213,7 @@ class _ClassFeedbackViewState extends State { } return null; }, - initialValues: initialValues[int.parse(question.id)], + answerValues: answerValues[int.parse(question.id)], ), const SizedBox(height: 10), ], @@ -299,10 +299,10 @@ class _ClassFeedbackViewState extends State { icon: Icons.info_outline, text: S.current.infoFormAnonymous, ), - classWidget(), - lecturerWidget(context), - assistantWidget(), - acknowledgementWidget(), + classFormField(), + lecturerFormField(context), + assistantFormField(), + acknowledgementFormField(), const SizedBox(height: 24), ]; @@ -313,7 +313,7 @@ class _ClassFeedbackViewState extends State { ]; for (final question in feedbackQuestions.values.where((q) => q.category == category)) { - categoryChildren.add(questionWidget(question)); + categoryChildren.add(questionFormField(question)); } children.add( Column( diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 2834a723b..190768922 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class EmojiFormField extends FormField> { EmojiFormField({ - @required Map initialValues, + @required Map answerValues, @required String question, FormFieldSetter> onSaved, String Function(Map) validator, @@ -13,7 +13,7 @@ class EmojiFormField extends FormField> { validator: validator, onSaved: onSaved, autovalidateMode: AutovalidateMode.onUserInteraction, - initialValue: initialValues, + initialValue: answerValues, builder: (state) { final context = state.context; final List emojis = [ From 71922d7b952e07ec81dce9486fb684825ae69861 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 12 May 2021 23:43:58 +0300 Subject: [PATCH 25/59] Create multiple question types --- .../model/class_feedback_answer.dart | 21 +++++-- .../model/questions/question.dart | 6 +- .../model/questions/question_dropdown.dart | 9 ++- .../model/questions/question_input.dart | 15 +++++ .../model/questions/question_rating.dart | 15 +++++ .../model/questions/question_text.dart | 15 +++++ .../service/feedback_provider.dart | 30 ++++++++-- .../view/class_feedback_view.dart | 60 +++++++------------ 8 files changed, 117 insertions(+), 54 deletions(-) create mode 100644 lib/pages/class_feedback/model/questions/question_input.dart create mode 100644 lib/pages/class_feedback/model/questions/question_rating.dart create mode 100644 lib/pages/class_feedback/model/questions/question_text.dart diff --git a/lib/pages/class_feedback/model/class_feedback_answer.dart b/lib/pages/class_feedback/model/class_feedback_answer.dart index 0bfe6105a..7fd9a14f8 100644 --- a/lib/pages/class_feedback/model/class_feedback_answer.dart +++ b/lib/pages/class_feedback/model/class_feedback_answer.dart @@ -1,19 +1,28 @@ import 'package:acs_upb_mobile/pages/people/model/person.dart'; -class ClassFeedbackAnswer { - ClassFeedbackAnswer({ - this.questionTextAnswer, - this.questionNumericAnswer, +class FeedbackQuestionAnswer { + FeedbackQuestionAnswer({ + this.questionAnswer, this.className, this.teacherName, this.assistant, this.questionNumber, }); - final String questionTextAnswer; - final String questionNumericAnswer; + String questionAnswer; final String className; final String teacherName; final Person assistant; final String questionNumber; + + @override + int get hashCode => questionAnswer.hashCode; + + @override + bool operator ==(Object other) { + if (other is FeedbackQuestionAnswer) { + return other.questionAnswer == questionAnswer; + } + return false; + } } diff --git a/lib/pages/class_feedback/model/questions/question.dart b/lib/pages/class_feedback/model/questions/question.dart index 2fa92cdcc..f54f379ed 100644 --- a/lib/pages/class_feedback/model/questions/question.dart +++ b/lib/pages/class_feedback/model/questions/question.dart @@ -1,15 +1,15 @@ class FeedbackQuestion { - const FeedbackQuestion({ + FeedbackQuestion({ this.question, this.category, - this.type, this.id, + this.answer, }); final String question; final String category; - final String type; final String id; + String answer; @override int get hashCode => question.hashCode; diff --git a/lib/pages/class_feedback/model/questions/question_dropdown.dart b/lib/pages/class_feedback/model/questions/question_dropdown.dart index 7b37a8227..90438a8cf 100644 --- a/lib/pages/class_feedback/model/questions/question_dropdown.dart +++ b/lib/pages/class_feedback/model/questions/question_dropdown.dart @@ -4,11 +4,16 @@ class FeedbackQuestionDropdown extends FeedbackQuestion { FeedbackQuestionDropdown({ String question, String category, - String type, String id, List answerOptions, + String answer, }) : options = answerOptions, - super(question: question, category: category, type: type, id: id); + super( + question: question, + category: category, + id: id, + answer: answer, + ); List options; } diff --git a/lib/pages/class_feedback/model/questions/question_input.dart b/lib/pages/class_feedback/model/questions/question_input.dart new file mode 100644 index 000000000..3bbbb1609 --- /dev/null +++ b/lib/pages/class_feedback/model/questions/question_input.dart @@ -0,0 +1,15 @@ +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; + +class FeedbackQuestionInput extends FeedbackQuestion { + FeedbackQuestionInput({ + String question, + String category, + String id, + String answer, + }) : super( + question: question, + category: category, + id: id, + answer: answer, + ); +} diff --git a/lib/pages/class_feedback/model/questions/question_rating.dart b/lib/pages/class_feedback/model/questions/question_rating.dart new file mode 100644 index 000000000..50cce2c7f --- /dev/null +++ b/lib/pages/class_feedback/model/questions/question_rating.dart @@ -0,0 +1,15 @@ +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; + +class FeedbackQuestionRating extends FeedbackQuestion { + FeedbackQuestionRating({ + String question, + String category, + String id, + String answer, + }) : super( + question: question, + category: category, + id: id, + answer: answer, + ); +} diff --git a/lib/pages/class_feedback/model/questions/question_text.dart b/lib/pages/class_feedback/model/questions/question_text.dart new file mode 100644 index 000000000..0bfb1405c --- /dev/null +++ b/lib/pages/class_feedback/model/questions/question_text.dart @@ -0,0 +1,15 @@ +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; + +class FeedbackQuestionText extends FeedbackQuestion { + FeedbackQuestionText({ + String question, + String category, + String id, + String answer, + }) : super( + question: question, + category: category, + id: id, + answer: answer, + ); +} diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 4e21b6843..d2f0b25b4 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -1,18 +1,20 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -extension ClassFeedbackAnswerExtension on ClassFeedbackAnswer { +extension ClassFeedbackAnswerExtension on FeedbackQuestionAnswer { Map toData() { final Map data = {}; - if (questionTextAnswer != null) data['answer'] = questionTextAnswer; - if (questionNumericAnswer != null) data['rating'] = questionNumericAnswer; + if (questionAnswer != null) data['answer'] = questionAnswer; data['dateSubmitted'] = Timestamp.now(); data['class'] = className; data['teacher'] = teacherName; @@ -31,15 +33,31 @@ extension FeedbackQuestionExtension on FeedbackQuestion { return FeedbackQuestionDropdown( category: json['category'], question: json['question'][LocaleProvider.localeString], - type: json['type'], id: id, answerOptions: optionsString, ); + } else if (json['type'] == 'rating') { + return FeedbackQuestionRating( + category: json['category'], + question: json['question'][LocaleProvider.localeString], + id: id, + ); + } else if (json['type'] == 'text') { + return FeedbackQuestionText( + category: json['category'], + question: json['question'][LocaleProvider.localeString], + id: id, + ); + } else if (json['type'] == 'input') { + return FeedbackQuestionInput( + category: json['category'], + question: json['question'][LocaleProvider.localeString], + id: id, + ); } else { return FeedbackQuestion( category: json['category'], question: json['question'][LocaleProvider.localeString], - type: json['type'], id: id, ); } @@ -47,7 +65,7 @@ extension FeedbackQuestionExtension on FeedbackQuestion { } class FeedbackProvider with ChangeNotifier { - Future addResponse(ClassFeedbackAnswer response) async { + Future addResponse(FeedbackQuestionAnswer response) async { try { await FirebaseFirestore.instance .collection('forms') diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 57760243c..8da7ce06c 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -1,6 +1,9 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; @@ -36,7 +39,6 @@ class _ClassFeedbackViewState extends State { Person selectedAssistant; List classTeachers = []; Map feedbackCategories = {}; - Map responses = {}; List> answerValues = []; Map feedbackQuestions = {}; @@ -163,7 +165,7 @@ class _ClassFeedbackViewState extends State { } Widget questionFormField(FeedbackQuestion question) { - if (question.type == 'input') { + if (question is FeedbackQuestionInput) { return Column( children: [ Text( @@ -178,7 +180,7 @@ class _ClassFeedbackViewState extends State { prefixIcon: const Icon(Icons.question_answer_outlined), ), onSaved: (value) { - responses[int.parse(question.id)] = value; + question.answer = value; }, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { @@ -197,13 +199,13 @@ class _ClassFeedbackViewState extends State { const SizedBox(height: 10), ], ); - } else if (question.type == 'rating') { + } else if (question is FeedbackQuestionRating) { return Column( children: [ EmojiFormField( question: question.question, onSaved: (value) { - responses[int.parse(question.id)] = value.keys + question.answer = value.keys .firstWhere((element) => value[element] == true) .toString(); }, @@ -218,7 +220,7 @@ class _ClassFeedbackViewState extends State { const SizedBox(height: 10), ], ); - } else if (question.type == 'dropdown') { + } else if (question is FeedbackQuestionDropdown) { return Column( children: [ Text( @@ -233,10 +235,9 @@ class _ClassFeedbackViewState extends State { prefixIcon: const Icon(Icons.list_outlined), ), onSaved: (value) { - responses[int.parse(question.id)] = value; + question.answer = value; }, - items: (question as FeedbackQuestionDropdown) - .options + items: question.options .map( (type) => DropdownMenuItem( value: type, @@ -258,7 +259,7 @@ class _ClassFeedbackViewState extends State { const SizedBox(height: 10), ], ); - } else if (question.type == 'text') { + } else if (question is FeedbackQuestionText) { return Column( children: [ Text( @@ -275,7 +276,7 @@ class _ClassFeedbackViewState extends State { children: [ TextFormField( onSaved: (value) { - responses[int.parse(question.id)] = value; + question.answer = value; }, keyboardType: TextInputType.multiline, maxLines: null, @@ -353,6 +354,7 @@ class _ClassFeedbackViewState extends State { text: S.current.buttonSend, onPressed: () async { if (!formKey.currentState.validate()) return; + if (!agreedToResponsibilities) { AppToast.show( '${S.current.warningAgreeTo}${S.current.labelFeedbackPolicy}.'); @@ -364,36 +366,20 @@ class _ClassFeedbackViewState extends State { }); bool res; - for (var i = 0; i < feedbackQuestions.length; i++) { res = false; - if (feedbackQuestions[i.toString()].type == 'input' || - feedbackQuestions[i.toString()].type == 'text' || - feedbackQuestions[i.toString()].type == 'dropdown') { - final response = ClassFeedbackAnswer( - assistant: selectedAssistant, - teacherName: selectedTeacherName, - className: classController.text, - questionNumber: i.toString(), - questionTextAnswer: responses[i], - ); - res = await Provider.of(context, listen: false) - .addResponse(response); - if (!res) break; - } else { - final response = ClassFeedbackAnswer( - assistant: selectedAssistant, - teacherName: selectedTeacherName, - className: classController.text, - questionNumber: i.toString(), - questionNumericAnswer: responses[i], - ); + final response = FeedbackQuestionAnswer( + assistant: selectedAssistant, + teacherName: selectedTeacherName, + className: classController.text, + questionNumber: i.toString(), + questionAnswer: feedbackQuestions[i.toString()].answer, + ); - res = await Provider.of(context, listen: false) - .addResponse(response); - if (!res) break; - } + res = await Provider.of(context, listen: false) + .addResponse(response); + if (!res) break; } if (res) { Navigator.of(context).pop(); From 04467882d57967bce326ee17598608c0f66c0efe Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Thu, 13 May 2021 14:48:52 +0300 Subject: [PATCH 26/59] Create separate question display widget --- .../view/class_feedback_view.dart | 139 +--------------- lib/widgets/feedback_question.dart | 151 ++++++++++++++++++ 2 files changed, 154 insertions(+), 136 deletions(-) create mode 100644 lib/widgets/feedback_question.dart diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 8da7ce06c..f683e2008 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -1,17 +1,13 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; +import 'package:acs_upb_mobile/widgets/feedback_question.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; -import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/cupertino.dart'; @@ -19,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:validators/validators.dart'; class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -164,135 +159,6 @@ class _ClassFeedbackViewState extends State { ); } - Widget questionFormField(FeedbackQuestion question) { - if (question is FeedbackQuestionInput) { - return Column( - children: [ - Text( - question.question, - style: const TextStyle( - fontSize: 18, - ), - ), - TextFormField( - decoration: InputDecoration( - labelText: S.current.labelAnswer, - prefixIcon: const Icon(Icons.question_answer_outlined), - ), - onSaved: (value) { - question.answer = value; - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { - if (value?.isEmpty ?? true) { - return S.current.errorAnswerCannotBeEmpty; - } - if (!isNumeric(value) || - int.parse(value) < 0 || - int.parse(value) > 10) { - return S.current.errorAnswerIncorrect; - } - return null; - }, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 10), - ], - ); - } else if (question is FeedbackQuestionRating) { - return Column( - children: [ - EmojiFormField( - question: question.question, - onSaved: (value) { - question.answer = value.keys - .firstWhere((element) => value[element] == true) - .toString(); - }, - validator: (selection) { - if (selection.values.where((e) => e != false).isEmpty) { - return S.current.warningYouNeedToSelectAtLeastOne; - } - return null; - }, - answerValues: answerValues[int.parse(question.id)], - ), - const SizedBox(height: 10), - ], - ); - } else if (question is FeedbackQuestionDropdown) { - return Column( - children: [ - Text( - question.question, - style: const TextStyle( - fontSize: 18, - ), - ), - DropdownButtonFormField( - decoration: InputDecoration( - labelText: S.current.labelAnswer, - prefixIcon: const Icon(Icons.list_outlined), - ), - onSaved: (value) { - question.answer = value; - }, - items: question.options - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type.toString()), - ), - ) - .toList(), - onChanged: (selection) { - formKey.currentState.validate(); - setState(() => {}); - }, - validator: (selection) { - if (selection == null) { - return S.current.errorAnswerCannotBeEmpty; - } - return null; - }, - ), - const SizedBox(height: 10), - ], - ); - } else if (question is FeedbackQuestionText) { - return Column( - children: [ - Text( - question.question, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - onSaved: (value) { - question.answer = value; - }, - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], - ), - ), - ), - const SizedBox(height: 24), - ], - ); - } else { - return null; - } - } - @override Widget build(BuildContext context) { final List children = [ @@ -314,7 +180,8 @@ class _ClassFeedbackViewState extends State { ]; for (final question in feedbackQuestions.values.where((q) => q.category == category)) { - categoryChildren.add(questionFormField(question)); + categoryChildren.add(FeedbackQuestionForm( + question: question, answerValues: answerValues, formKey: formKey)); } children.add( Column( diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart new file mode 100644 index 000000000..6bd3c6b25 --- /dev/null +++ b/lib/widgets/feedback_question.dart @@ -0,0 +1,151 @@ +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; +import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:validators/validators.dart'; +import 'package:acs_upb_mobile/generated/l10n.dart'; + +class FeedbackQuestionForm extends StatelessWidget { + const FeedbackQuestionForm({ + this.question, + this.answerValues, + this.formKey, + }); + + final FeedbackQuestion question; + final List> answerValues; + final GlobalKey formKey; + + @override + Widget build(BuildContext context) { + if (question is FeedbackQuestionInput) { + return Column( + children: [ + Text( + question.question, + style: const TextStyle( + fontSize: 18, + ), + ), + TextFormField( + decoration: InputDecoration( + labelText: S.current.labelAnswer, + prefixIcon: const Icon(Icons.question_answer_outlined), + ), + onSaved: (value) { + question.answer = value; + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value?.isEmpty ?? true) { + return S.current.errorAnswerCannotBeEmpty; + } + if (!isNumeric(value) || + int.parse(value) < 0 || + int.parse(value) > 10) { + return S.current.errorAnswerIncorrect; + } + return null; + }, + ), + const SizedBox(height: 10), + ], + ); + } else if (question is FeedbackQuestionRating) { + return Column( + children: [ + EmojiFormField( + question: question.question, + onSaved: (value) { + question.answer = value.keys + .firstWhere((element) => value[element] == true) + .toString(); + }, + validator: (selection) { + if (selection.values.where((e) => e != false).isEmpty) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, + answerValues: answerValues[int.parse(question.id)], + ), + const SizedBox(height: 10), + ], + ); + } else if (question is FeedbackQuestionDropdown) { + return Column( + children: [ + Text( + question.question, + style: const TextStyle( + fontSize: 18, + ), + ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelAnswer, + prefixIcon: const Icon(Icons.list_outlined), + ), + onSaved: (value) { + question.answer = value; + }, + items: (question as FeedbackQuestionDropdown) + .options + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + }, + validator: (selection) { + if (selection == null) { + return S.current.errorAnswerCannotBeEmpty; + } + return null; + }, + ), + const SizedBox(height: 10), + ], + ); + } else if (question is FeedbackQuestionText) { + return Column( + children: [ + Text( + question.question, + style: const TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + onSaved: (value) { + question.answer = value; + }, + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + ], + ); + } else { + return null; + } + } +} From a0d58e0a5b6484bc1660674817350978c04abd3b Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Thu, 13 May 2021 16:46:37 +0300 Subject: [PATCH 27/59] Refactor emojis animation --- lib/widgets/radio_emoji.dart | 47 +++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 190768922..0b63d3714 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -20,27 +20,27 @@ class EmojiFormField extends FormField> { const Icon( Icons.sentiment_very_dissatisfied, color: Colors.red, - size: 30, + size: 29, ), const Icon( Icons.sentiment_dissatisfied, color: Colors.redAccent, - size: 30, + size: 29, ), const Icon( Icons.sentiment_neutral, color: Colors.amber, - size: 30, + size: 29, ), const Icon( Icons.sentiment_satisfied, color: Colors.lightGreen, - size: 30, + size: 29, ), const Icon( Icons.sentiment_very_satisfied, color: Colors.green, - size: 30, + size: 29, ) ]; final List emojiControllers = @@ -81,7 +81,7 @@ class EmojiFormField extends FormField> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 24), + const SizedBox(height: 12), Text( '$question', style: const TextStyle( @@ -95,7 +95,6 @@ class EmojiFormField extends FormField> { children: emojiSelectables, ), ), - const SizedBox(height: 12), if (state.hasError) Padding( padding: const EdgeInsets.only(top: 8), @@ -130,27 +129,47 @@ class SelectableIcon extends Selectable { _SelectableIconState createState() => _SelectableIconState(icon); } -class _SelectableIconState extends SelectableState { +class _SelectableIconState extends SelectableState + with SingleTickerProviderStateMixin { _SelectableIconState(this.icon); Icon icon; + AnimationController animationController; + CurvedAnimation animation; @override void initState() { super.initState(); isSelected = widget.initiallySelected; + animationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ); + animation = CurvedAnimation( + parent: animationController, + curve: Curves.elasticOut, + ); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { widget.controller.selectableState = this; + if (!isSelected) animationController.value = 0; + return GestureDetector( onTap: () { setState( () { isSelected = !isSelected; widget.onSelected(isSelected); + animationController.forward(); }, ); }, @@ -160,11 +179,15 @@ class _SelectableIconState extends SelectableState { shape: BoxShape.circle, ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: AnimatedContainer( - height: isSelected ? 40 : 10, - width: isSelected ? 70 : 30, - duration: const Duration(milliseconds: 500), + child: AnimatedBuilder( + animation: animation, child: icon, + builder: (BuildContext context, Widget child) { + return Transform.scale( + scale: isSelected ? animation.value * 0.6 + 1 : 1, + child: child, + ); + }, ), ), ); From 8859e869162737fd45e49996559c4911628acbe6 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Thu, 13 May 2021 17:21:32 +0300 Subject: [PATCH 28/59] Realign message at the beginning --- .../class_feedback/view/class_feedback_view.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index f683e2008..38588ea57 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -162,10 +162,6 @@ class _ClassFeedbackViewState extends State { @override Widget build(BuildContext context) { final List children = [ - IconText( - icon: Icons.info_outline, - text: S.current.infoFormAnonymous, - ), classFormField(), lecturerFormField(context), assistantFormField(), @@ -205,6 +201,14 @@ class _ClassFeedbackViewState extends State { actions: [_submitButton()], body: ListView( children: [ + Padding( + padding: const EdgeInsets.only(left: 25, top: 10), + child: IconText( + icon: Icons.info_outline, + text: S.current.infoFormAnonymous, + style: Theme.of(context).textTheme.bodyText1, + ), + ), Padding( padding: const EdgeInsets.all(10), child: Form( From 0b84ebd72d827d10e159b4bd921335d2687c25d5 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Mon, 17 May 2021 20:47:14 +0300 Subject: [PATCH 29/59] Center anonymous form notice. --- .../class_feedback/view/class_feedback_view.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 38588ea57..16ca69f98 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; @@ -14,7 +15,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; -import 'package:acs_upb_mobile/generated/l10n.dart'; class ClassFeedbackView extends StatefulWidget { const ClassFeedbackView({Key key, this.classHeader}) : super(key: key); @@ -201,12 +201,14 @@ class _ClassFeedbackViewState extends State { actions: [_submitButton()], body: ListView( children: [ - Padding( - padding: const EdgeInsets.only(left: 25, top: 10), - child: IconText( - icon: Icons.info_outline, - text: S.current.infoFormAnonymous, - style: Theme.of(context).textTheme.bodyText1, + Center( + child: Padding( + padding: const EdgeInsets.only(left: 25, top: 10), + child: IconText( + icon: Icons.info_outline, + text: S.current.infoFormAnonymous, + style: Theme.of(context).textTheme.bodyText1, + ), ), ), Padding( From a61078b2c0e42182eb61ca9d4f95b08f703253e7 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Mon, 17 May 2021 21:36:49 +0300 Subject: [PATCH 30/59] Improve emoji animation and remove Selectable. --- lib/widgets/radio_emoji.dart | 74 ++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 0b63d3714..6476c3d27 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -1,4 +1,3 @@ -import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:flutter/material.dart'; class EmojiFormField extends FormField> { @@ -24,7 +23,7 @@ class EmojiFormField extends FormField> { ), const Icon( Icons.sentiment_dissatisfied, - color: Colors.redAccent, + color: Colors.orange, size: 29, ), const Icon( @@ -43,8 +42,8 @@ class EmojiFormField extends FormField> { size: 29, ) ]; - final List emojiControllers = - emojis.map((_) => SelectableController()).toList(); + final List emojiControllers = + emojis.map((_) => SelectableIconController()).toList(); final emojiSelectables = emojiControllers .asMap() @@ -112,43 +111,61 @@ class EmojiFormField extends FormField> { ); } -class SelectableIcon extends Selectable { - const SelectableIcon({ - bool initiallySelected, - void Function(bool) onSelected, +class SelectableIconController { + _SelectableIconState _state; + + bool get isSelected => _state?.isSelected; + + void select() { + if (_state == null) return; + if (!isSelected) { + _state.isSelected = true; + _state.animationController.forward(); + } + } + + void deselect() { + if (_state == null) return; + if (isSelected) { + _state.isSelected = false; + _state.animationController.reverse(); + } + } +} + +class SelectableIcon extends StatefulWidget { + SelectableIcon({ + this.onSelected, this.icon, - SelectableController controller, - }) : super( - initiallySelected: initiallySelected ?? false, - onSelected: onSelected, - controller: controller); + SelectableIconController controller, + }) : controller = controller ?? SelectableIconController(); + final void Function(bool) onSelected; final Icon icon; + final SelectableIconController controller; @override - _SelectableIconState createState() => _SelectableIconState(icon); + State createState() => _SelectableIconState(icon); } -class _SelectableIconState extends SelectableState +class _SelectableIconState extends State with SingleTickerProviderStateMixin { _SelectableIconState(this.icon); Icon icon; AnimationController animationController; - CurvedAnimation animation; + Animation animation; + bool isSelected; @override void initState() { super.initState(); - isSelected = widget.initiallySelected; + isSelected = false; animationController = AnimationController( vsync: this, - duration: const Duration(seconds: 3), - ); - animation = CurvedAnimation( - parent: animationController, - curve: Curves.elasticOut, + duration: const Duration(milliseconds: 300), ); + animation = Tween(begin: 1, end: 1.5).animate(animationController); } @override @@ -159,17 +176,18 @@ class _SelectableIconState extends SelectableState @override Widget build(BuildContext context) { - widget.controller.selectableState = this; - - if (!isSelected) animationController.value = 0; + widget.controller._state = this; return GestureDetector( onTap: () { setState( () { - isSelected = !isSelected; + if (isSelected) { + widget.controller.deselect(); + } else { + widget.controller.select(); + } widget.onSelected(isSelected); - animationController.forward(); }, ); }, @@ -184,7 +202,7 @@ class _SelectableIconState extends SelectableState child: icon, builder: (BuildContext context, Widget child) { return Transform.scale( - scale: isSelected ? animation.value * 0.6 + 1 : 1, + scale: animation.value, child: child, ); }, From b128c4cd3d409499fe493804b1995e0081bbcb8c Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Tue, 18 May 2021 22:47:36 +0300 Subject: [PATCH 31/59] Modify feedback policy message --- lib/generated/intl/messages_en.dart | 2 +- lib/generated/intl/messages_ro.dart | 2 +- lib/generated/l10n.dart | 4 ++-- lib/l10n/intl_en.arb | 2 +- lib/l10n/intl_ro.arb | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index cc71ff5e8..adc1d6d10 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -183,7 +183,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageAccountCreated" : MessageLookupByLibrary.simpleMessage("Account created successfully."), "messageAccountDeleted" : MessageLookupByLibrary.simpleMessage("Account deleted successfully."), "messageAddCustomWebsite" : MessageLookupByLibrary.simpleMessage("Try adding a custom website."), - "messageAgreeFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words."), + "messageAgreeFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("I understand this survey is extremely important for the continuous development of the educational process and I will only provide valuable and constructive feedback for this class."), "messageAgreePermissions" : MessageLookupByLibrary.simpleMessage("I will only upload information that is correct and accurate, to the best of my knowledge. I understand that submitting erroneous or offensive information on purpose will lead to my editing permissions being permanently revoked."), "messageAnnouncedOnMail" : MessageLookupByLibrary.simpleMessage("You will receive a mail confirmation if your request is approved."), "messageAskPermissionToEdit" : m6, diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index fa397cdc3..ddc6e8906 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -183,7 +183,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageAccountCreated" : MessageLookupByLibrary.simpleMessage("Contul a fost creat cu succes."), "messageAccountDeleted" : MessageLookupByLibrary.simpleMessage("Contul a fost șters cu succes."), "messageAddCustomWebsite" : MessageLookupByLibrary.simpleMessage("Încercați să adăugați un website."), - "messageAgreeFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("Înțeleg că acest sondaj este extrem de important pentru dezvoltarea continuă a procesului educațional și nu voi adresa insulte sau folosi cuvinte obscene."), + "messageAgreeFeedbackPolicy" : MessageLookupByLibrary.simpleMessage("Înțeleg că acest sondaj este extrem de important pentru dezvoltarea continuă a procesului educațional și voi oferi doar feedback valoros și constructiv pentru această materie."), "messageAgreePermissions" : MessageLookupByLibrary.simpleMessage("Voi încărca doar informații corecte si precise. Înțeleg că încărcarea informațiilor eronate sau ofensatoare în mod intenționat va conduce la blocarea permisiunilor mele permanent."), "messageAnnouncedOnMail" : MessageLookupByLibrary.simpleMessage("Veți primi o confirmare pe mail dacă vi se acceptă cererea."), "messageAskPermissionToEdit" : m6, diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 3011c6a8f..728d1dfae 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2555,10 +2555,10 @@ class S { ); } - /// `I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words.` + /// `I understand this survey is extremely important for the continuous development of the educational process and I will only provide valuable and constructive feedback for this class.` String get messageAgreeFeedbackPolicy { return Intl.message( - 'I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words.', + 'I understand this survey is extremely important for the continuous development of the educational process and I will only provide valuable and constructive feedback for this class.', name: 'messageAgreeFeedbackPolicy', desc: '', args: [], diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 658dfb0ad..24ddeb2aa 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -266,7 +266,7 @@ "messageChangePasswordSuccess": "Password changed successfully.", "messageChangeEmailSuccess": "Email changed successfully", "messageAgreePermissions": "I will only upload information that is correct and accurate, to the best of my knowledge. I understand that submitting erroneous or offensive information on purpose will lead to my editing permissions being permanently revoked.", - "messageAgreeFeedbackPolicy": "I understand this survey is extremely important for the continuous development of the educational process and I will not address insults or use any obscene words.", + "messageAgreeFeedbackPolicy": "I understand this survey is extremely important for the continuous development of the educational process and I will only provide valuable and constructive feedback for this class.", "messageYouCanContribute": "You can contribute to the app data, but you first need to request permissions.", "messageThereAreNoEventsForSelected": "There are no events for the selected ", "messagePictureUpdatedSuccess": "Profile picture updated successfully.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index ba8e5ca3b..fc0084d55 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -266,7 +266,7 @@ "messageChangePasswordSuccess": "Parola a fost schimbată cu succes.", "messageChangeEmailSuccess": "Email-ul a fost schimbat cu succes", "messageAgreePermissions": "Voi încărca doar informații corecte si precise. Înțeleg că încărcarea informațiilor eronate sau ofensatoare în mod intenționat va conduce la blocarea permisiunilor mele permanent.", - "messageAgreeFeedbackPolicy": "Înțeleg că acest sondaj este extrem de important pentru dezvoltarea continuă a procesului educațional și nu voi adresa insulte sau folosi cuvinte obscene.", + "messageAgreeFeedbackPolicy": "Înțeleg că acest sondaj este extrem de important pentru dezvoltarea continuă a procesului educațional și voi oferi doar feedback valoros și constructiv pentru această materie.", "messageYouCanContribute": "Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni.", "messageThereAreNoEventsForSelected": "Nu există evenimente pentru selecția de ", "messagePictureUpdatedSuccess": "Poza a fost actualizată cu succes.", From 28ef2f7645d9f404bd58e0a75c3fae4a9893b04d Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 19 May 2021 01:50:04 +0300 Subject: [PATCH 32/59] Allow users to submit one time only feedback for a class --- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ro.dart | 3 +- lib/generated/l10n.dart | 10 +++++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ro.arb | 3 +- .../service/feedback_provider.dart | 42 ++++++++++++------- .../view/class_feedback_view.dart | 18 +++++--- lib/pages/classes/view/class_view.dart | 24 ++++++++--- 8 files changed, 74 insertions(+), 28 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index adc1d6d10..d7081d715 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -290,6 +290,7 @@ class MessageLookup extends MessageLookupByLibrary { "warningEmailInUse" : m10, "warningEventNotEditable" : MessageLookupByLibrary.simpleMessage("This event cannot be edited."), "warningFavouriteWebsitesInitializationFailed" : MessageLookupByLibrary.simpleMessage("Could not read favourite websites."), + "warningFeedbackAlreadySent" : MessageLookupByLibrary.simpleMessage("You have already submitted feedback for this class!"), "warningFieldCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Field cannot be empty."), "warningFieldCannotBeZero" : MessageLookupByLibrary.simpleMessage("Field cannot be zero."), "warningFilterAlreadyDisabled" : MessageLookupByLibrary.simpleMessage("Already showing all content."), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index ddc6e8906..ea097ccf1 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -203,7 +203,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Eveniment adăugat cu succes."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Eveniment șters cu succes."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Eveniment modificat cu succes."), - "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("Recenzia a fost trimisă cu succes."), + "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("Feedback trimis cu succes."), "messageGetStartedByPressing" : MessageLookupByLibrary.simpleMessage("Începeți prin a apăsa butonul"), "messageIAgreeToThe" : MessageLookupByLibrary.simpleMessage("Sunt de acord cu "), "messageNewUser" : MessageLookupByLibrary.simpleMessage("Utilizator nou?"), @@ -290,6 +290,7 @@ class MessageLookup extends MessageLookupByLibrary { "warningEmailInUse" : m10, "warningEventNotEditable" : MessageLookupByLibrary.simpleMessage("Acest eveniment nu poate fi modificat."), "warningFavouriteWebsitesInitializationFailed" : MessageLookupByLibrary.simpleMessage("Nu se pot citi date despre site-urile favorite."), + "warningFeedbackAlreadySent" : MessageLookupByLibrary.simpleMessage("Ați trimis deja feedback pentru această materie!"), "warningFieldCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Câmpul nu poate fi gol."), "warningFieldCannotBeZero" : MessageLookupByLibrary.simpleMessage("Câmpul nu poate fi zero."), "warningFilterAlreadyDisabled" : MessageLookupByLibrary.simpleMessage("Întreg conținutul este vizibil deja."), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 728d1dfae..15dc010ea 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1375,6 +1375,16 @@ class S { ); } + /// `You have already submitted feedback for this class!` + String get warningFeedbackAlreadySent { + return Intl.message( + 'You have already submitted feedback for this class!', + name: 'warningFeedbackAlreadySent', + desc: '', + args: [], + ); + } + /// `Already showing only custom websites.` String get warningFilterAlreadyShowingYours { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 24ddeb2aa..f3d0a93e9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -140,6 +140,7 @@ "warningUseProvider": "Please log in with {provider} to continue.", "warningTryAgainLater": "Please try again later.", "warningFilterAlreadyDisabled": "Already showing all content.", + "warningFeedbackAlreadySent": "You have already submitted feedback for this class!", "warningFilterAlreadyShowingYours": "Already showing only custom websites.", "warningInvalidURL": "You need to provide a valid URL.", "warningWebsiteNameExists": "A website with the same name already exists.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index fc0084d55..11644261d 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -140,6 +140,7 @@ "warningUseProvider": "Folosiți {provider} pentru a vă conecta.", "warningTryAgainLater": "Încercați mai târziu.", "warningFilterAlreadyDisabled": "Întreg conținutul este vizibil deja.", + "warningFeedbackAlreadySent": "Ați trimis deja feedback pentru această materie!", "warningFilterAlreadyShowingYours": "Deja sunt vizibile doar site-urile personale.", "warningInvalidURL": "Trebuie să introduceți un URL valid.", "warningWebsiteNameExists": "Există deja un site cu același nume.", @@ -258,7 +259,7 @@ "messageButtonAbove": "de mai sus.", "messageAskPermissionToEdit": "De ce dorești să primești permisiuni de editare în {appName}?", "messageRequestHasBeenSent": "Cererea a fost transmisă cu succes", - "messageFeedbackHasBeenSent": "Recenzia a fost trimisă cu succes.", + "messageFeedbackHasBeenSent": "Feedback trimis cu succes.", "messageRequestAlreadyExists": "Ați trimis deja o cerere. Daca doriți să adăugați una nouă, vă rugăm sa apasați 'Salvare'.", "messageIAgreeToThe": "Sunt de acord cu ", "messageEditProfileSuccess": "Profilul a fost actualizat cu succes.", diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index d2f0b25b4..87d809dd8 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -115,21 +115,33 @@ class FeedbackProvider with ChangeNotifier { } } - List getQuestionsByCategoryAndType( - List questions, String category, String type) { - final List filteredQuestions = []; - final List filterQuestions = questions - .where((element) => - element is Map && - element['category'] == category && - element['type'] == type) - .toList(); - for (final Map element in filterQuestions) { - final List qs = element.values.toList(); - filteredQuestions.add( - qs[qs.indexWhere((element) => element is Map)] - [LocaleProvider.localeString]); + Future setUserClassFeedback(String className, String uid) async { + try { + final DocumentReference ref = + FirebaseFirestore.instance.collection('users').doc(uid); + await ref.set({ + 'classesFeedback': {className: true} + }, SetOptions(merge: true)); + notifyListeners(); + return true; + } catch (e) { + AppToast.show(S.current.errorSomethingWentWrong); + return false; + } + } + + Future checkProvidedClassFeedback(String className, String uid) async { + try { + final DocumentSnapshot snap = + await FirebaseFirestore.instance.collection('users').doc(uid).get(); + if (snap.data()['classesFeedback'] != null && + snap.data()['classesFeedback'][className] == true) { + return true; + } + return false; + } catch (e) { + AppToast.show(S.current.errorSomethingWentWrong); + return false; } - return filteredQuestions; } } diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 16ca69f98..527b5070b 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; @@ -238,9 +239,12 @@ class _ClassFeedbackViewState extends State { formKey.currentState.save(); }); - bool res; + bool res1, res2; + final authProvider = + Provider.of(context, listen: false); + final String uid = authProvider.uid; for (var i = 0; i < feedbackQuestions.length; i++) { - res = false; + res1 = false; final response = FeedbackQuestionAnswer( assistant: selectedAssistant, @@ -250,11 +254,15 @@ class _ClassFeedbackViewState extends State { questionAnswer: feedbackQuestions[i.toString()].answer, ); - res = await Provider.of(context, listen: false) + res1 = await Provider.of(context, listen: false) .addResponse(response); - if (!res) break; + if (!res1) break; } - if (res) { + + res2 = await Provider.of(context, listen: false) + .setUserClassFeedback(classController.text, uid); + + if (res1 && res2) { Navigator.of(context).pop(); AppToast.show(S.current.messageFeedbackHasBeenSent); } diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index f8f605731..b8739a278 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -1,5 +1,6 @@ import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; @@ -33,6 +34,7 @@ class ClassView extends StatefulWidget { class _ClassViewState extends State { Class classInfo; String lecturerName = ''; + bool alreadyCompletedFeedback; @override void initState() { @@ -46,6 +48,10 @@ class _ClassViewState extends State { @override Widget build(BuildContext context) { final classProvider = Provider.of(context); + Provider.of(context, listen: false) + .checkProvidedClassFeedback(widget.classHeader.id, + Provider.of(context, listen: false).uid) + .then((value) => alreadyCompletedFeedback = value); return AppScaffold( title: Text(S.current.navigationClassInfo), @@ -54,12 +60,18 @@ class _ClassViewState extends State { AppScaffoldAction( icon: Icons.rate_review_outlined, onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - ClassFeedbackView(classHeader: widget.classHeader), - ), - ); + if (!alreadyCompletedFeedback) { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (_) => ClassFeedbackView( + classHeader: widget.classHeader), + ), + ) + .then((value) => setState(() {})); + } else { + AppToast.show(S.current.warningFeedbackAlreadySent); + } }), ], body: FutureBuilder( From 77c018bf82945920a938d3874afee4ec6a06f0ae Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 19 May 2021 12:25:47 +0300 Subject: [PATCH 33/59] Add tests for feedback page --- lib/pages/classes/view/class_view.dart | 8 +- lib/widgets/feedback_question.dart | 5 +- test/integration_test.dart | 154 +++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 4 deletions(-) diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index b8739a278..b308c8b4d 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -48,9 +48,11 @@ class _ClassViewState extends State { @override Widget build(BuildContext context) { final classProvider = Provider.of(context); - Provider.of(context, listen: false) - .checkProvidedClassFeedback(widget.classHeader.id, - Provider.of(context, listen: false).uid) + final feedbackProvider = + Provider.of(context, listen: false); + final authProvider = Provider.of(context, listen: false); + feedbackProvider + .checkProvidedClassFeedback(widget.classHeader.id, authProvider.uid) .then((value) => alreadyCompletedFeedback = value); return AppScaffold( diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index 6bd3c6b25..d89bd7f56 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -32,6 +32,7 @@ class FeedbackQuestionForm extends StatelessWidget { ), ), TextFormField( + key: const Key('FeedbackInput'), decoration: InputDecoration( labelText: S.current.labelAnswer, prefixIcon: const Icon(Icons.question_answer_outlined), @@ -86,6 +87,7 @@ class FeedbackQuestionForm extends StatelessWidget { ), ), DropdownButtonFormField( + key: const Key('FeedbackDropdown'), decoration: InputDecoration( labelText: S.current.labelAnswer, prefixIcon: const Icon(Icons.list_outlined), @@ -131,6 +133,7 @@ class FeedbackQuestionForm extends StatelessWidget { child: Column( children: [ TextFormField( + key: const Key('FeedbackText'), onSaved: (value) { question.answer = value; }, @@ -145,7 +148,7 @@ class FeedbackQuestionForm extends StatelessWidget { ], ); } else { - return null; + return Container(); } } } diff --git a/test/integration_test.dart b/test/integration_test.dart index 6080a52a1..2305598ae 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -2,6 +2,12 @@ import 'package:acs_upb_mobile/authentication/model/user.dart'; import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; import 'package:acs_upb_mobile/main.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; @@ -40,6 +46,7 @@ import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; import 'package:acs_upb_mobile/pages/timetable/view/timetable_page.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; +import 'package:acs_upb_mobile/widgets/feedback_question.dart'; import 'package:acs_upb_mobile/widgets/search_bar.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; @@ -80,6 +87,8 @@ class MockRequestProvider extends Mock implements RequestProvider {} class MockNavigatorObserver extends Mock implements NavigatorObserver {} +class MockFeedbackProvider extends Mock implements FeedbackProvider {} + Future main() async { AuthProvider mockAuthProvider; WebsiteProvider mockWebsiteProvider; @@ -90,6 +99,7 @@ Future main() async { MockNewsProvider mockNewsProvider; UniEventProvider mockEventProvider; RequestProvider mockRequestProvider; + FeedbackProvider mockFeedbackProvider; setupFirebaseAuthMocks(); await Firebase.initializeApp(); @@ -127,6 +137,8 @@ Future main() async { ChangeNotifierProvider( create: (_) => mockEventProvider), Provider(create: (_) => mockRequestProvider), + ChangeNotifierProvider( + create: (_) => mockFeedbackProvider), ], child: const MyApp(), ); @@ -399,6 +411,51 @@ Future main() async { when(mockPersonProvider.mostRecentLecturer(any)) .thenAnswer((_) => Future.value('Jane Doe')); + mockFeedbackProvider = MockFeedbackProvider(); + // ignore: invalid_use_of_protected_member + when(mockFeedbackProvider.hasListeners).thenReturn(true); + when(mockFeedbackProvider.fetchQuestions()).thenAnswer((_) => Future.value({ + '0': FeedbackQuestionDropdown( + category: 'involvement', + question: + 'Approximate number of activities that you attended (lectures + applications):', + id: '0', + answerOptions: ['option 1', 'option 2', 'option 3', 'option 4'], + ), + '1': FeedbackQuestionRating( + category: 'applications', + question: 'Was the exposure method appropriate?', + id: '1', + ), + '2': FeedbackQuestionText( + category: 'personal', + question: 'What are the positive aspects of this class?', + id: '2', + ), + '3': FeedbackQuestionInput( + category: 'homework', + question: + 'Estimate the average number of hours per week devoted to solving homework.', + id: '3', + ), + })); + when(mockFeedbackProvider.fetchCategories()) + .thenAnswer((_) => Future.value({ + 'applications': {'en': 'Applications', 'ro': 'Aplicații'}, + 'homework': {'en': 'Homework', 'ro': 'Temă'}, + 'involvement': {'en': 'Involvement', 'ro': 'Implicare'}, + 'personal': { + 'en': 'Personal comments', + 'ro': 'Comentarii personale' + }, + })); + when(mockFeedbackProvider.addResponse(any)) + .thenAnswer((_) => Future.value(true)); + when(mockFeedbackProvider.setUserClassFeedback(any, any)) + .thenAnswer((_) => Future.value(true)); + when(mockFeedbackProvider.checkProvidedClassFeedback(any, any)) + .thenAnswer((_) => Future.value(false)); + mockQuestionProvider = MockQuestionProvider(); // ignore: invalid_use_of_protected_member when(mockQuestionProvider.hasListeners).thenReturn(false); @@ -1393,6 +1450,103 @@ Future main() async { }); }); + group('Feedback view', () { + setUp(() { + when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( + uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); + when(mockAuthProvider.currentUserFromCache).thenReturn(User( + uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); + when(mockAuthProvider.isAuthenticated).thenReturn(true); + when(mockAuthProvider.isAnonymous).thenReturn(false); + when(mockAuthProvider.uid).thenReturn('0'); + }); + + for (final size in screenSizes) { + testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { + await binding.setSurfaceSize(size); + + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + // Open timetable + await tester.tap(find.byIcon(Icons.calendar_today_outlined)); + await tester.pumpAndSettle(); + + // Open classes + await tester.tap(find.byIcon(FeatherIcons.bookOpen)); + await tester.pumpAndSettle(); + + // Open class view + await tester.tap(find.text('PC')); + await tester.pumpAndSettle(); + + expect(find.byType(ClassView), findsOneWidget); + + // Open feedback page + await tester.tap(find.byIcon(Icons.rate_review_outlined)); + await tester.pumpAndSettle(); + + expect(find.byType(ClassFeedbackView), findsOneWidget); + + await tester.tap(find.byType(Checkbox)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('AutocompleteAssistant')), 'John'); + await tester.pumpAndSettle(); + await tester.tap(find.text('John Doe')); + await tester.pumpAndSettle(); + + expect(find.byType(Card), findsNWidgets(5)); + expect(find.byType(FeedbackQuestionForm), findsNWidgets(4)); + expect( + find.text( + 'Estimate the average number of hours per week devoted to solving homework.'), + findsOneWidget); + expect( + find.text( + 'Approximate number of activities that you attended (lectures + applications):'), + findsOneWidget); + expect( + find.text('Was the exposure method appropriate?'), findsOneWidget); + expect(find.text('What are the positive aspects of this class?'), + findsOneWidget); + + await tester.enterText(find.byKey(const Key('FeedbackInput')), '2'); + await tester.pumpAndSettle(); + + expect(find.text('2'), findsOneWidget); + + await tester.tap(find.text('Send')); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(find.text('Answer cannot be empty.'), findsOneWidget); + + await tester.tap(find.byIcon(Icons.sentiment_very_satisfied)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('FeedbackText')), 'Best class ever!'); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('FeedbackDropdown'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('option 3').last); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Send')); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + expect(find.text('You need to select your assistant for this class.'), + findsNothing); + expect( + find.text('You need to select at least one option.'), findsNothing); + expect(find.text('Answer cannot be empty.'), findsNothing); + + expect(find.byType(ClassView), findsOneWidget); + }); + } + }); + group('Settings', () { for (final size in screenSizes) { testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { From 5ffb5150ef05dfa47b1aef6874fe21a56ce8bbf9 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 19 May 2021 14:42:58 +0300 Subject: [PATCH 34/59] Align questions to the left --- lib/widgets/feedback_question.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index d89bd7f56..53e20ca03 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -24,6 +24,7 @@ class FeedbackQuestionForm extends StatelessWidget { Widget build(BuildContext context) { if (question is FeedbackQuestionInput) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( question.question, @@ -58,6 +59,7 @@ class FeedbackQuestionForm extends StatelessWidget { ); } else if (question is FeedbackQuestionRating) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ EmojiFormField( question: question.question, @@ -79,6 +81,7 @@ class FeedbackQuestionForm extends StatelessWidget { ); } else if (question is FeedbackQuestionDropdown) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( question.question, @@ -119,6 +122,7 @@ class FeedbackQuestionForm extends StatelessWidget { ); } else if (question is FeedbackQuestionText) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( question.question, From aefe3b30e1634ebc1a4d4ffada1f2ced48ed776e Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 19 May 2021 15:04:20 +0300 Subject: [PATCH 35/59] Remove unused onChanged methods --- lib/pages/class_feedback/view/class_feedback_view.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 527b5070b..7cc3ac481 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -82,7 +82,6 @@ class _ClassFeedbackViewState extends State { labelText: S.current.labelClass, prefixIcon: const Icon(FeatherIcons.bookOpen), ), - onChanged: (_) => setState(() {}), ); } @@ -102,7 +101,6 @@ class _ClassFeedbackViewState extends State { labelText: S.current.labelLecturer, prefixIcon: const Icon(Icons.person_outline), ), - onChanged: (_) => setState(() {}), ); } else { return const Center(child: CircularProgressIndicator()); From 69d32aa5bd273e62c0e618ad286d6a557c895709 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 19 May 2021 15:45:13 +0300 Subject: [PATCH 36/59] Remove unused code --- .../class_feedback/model/class_feedback_answer.dart | 11 ----------- .../class_feedback/model/questions/question.dart | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/lib/pages/class_feedback/model/class_feedback_answer.dart b/lib/pages/class_feedback/model/class_feedback_answer.dart index 7fd9a14f8..97a664d9d 100644 --- a/lib/pages/class_feedback/model/class_feedback_answer.dart +++ b/lib/pages/class_feedback/model/class_feedback_answer.dart @@ -14,15 +14,4 @@ class FeedbackQuestionAnswer { final String teacherName; final Person assistant; final String questionNumber; - - @override - int get hashCode => questionAnswer.hashCode; - - @override - bool operator ==(Object other) { - if (other is FeedbackQuestionAnswer) { - return other.questionAnswer == questionAnswer; - } - return false; - } } diff --git a/lib/pages/class_feedback/model/questions/question.dart b/lib/pages/class_feedback/model/questions/question.dart index f54f379ed..1b224c26e 100644 --- a/lib/pages/class_feedback/model/questions/question.dart +++ b/lib/pages/class_feedback/model/questions/question.dart @@ -10,15 +10,4 @@ class FeedbackQuestion { final String category; final String id; String answer; - - @override - int get hashCode => question.hashCode; - - @override - bool operator ==(Object other) { - if (other is FeedbackQuestion) { - return other.question == question; - } - return false; - } } From 2a64481a8a93065ec89ad98c416599d961c49fd2 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Fri, 21 May 2021 08:57:50 +0300 Subject: [PATCH 37/59] Remove rating questions validator --- lib/widgets/feedback_question.dart | 6 ------ lib/widgets/radio_emoji.dart | 2 -- 2 files changed, 8 deletions(-) diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index 53e20ca03..666271791 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -68,12 +68,6 @@ class FeedbackQuestionForm extends StatelessWidget { .firstWhere((element) => value[element] == true) .toString(); }, - validator: (selection) { - if (selection.values.where((e) => e != false).isEmpty) { - return S.current.warningYouNeedToSelectAtLeastOne; - } - return null; - }, answerValues: answerValues[int.parse(question.id)], ), const SizedBox(height: 10), diff --git a/lib/widgets/radio_emoji.dart b/lib/widgets/radio_emoji.dart index 6476c3d27..751500a28 100644 --- a/lib/widgets/radio_emoji.dart +++ b/lib/widgets/radio_emoji.dart @@ -5,11 +5,9 @@ class EmojiFormField extends FormField> { @required Map answerValues, @required String question, FormFieldSetter> onSaved, - String Function(Map) validator, Key key, }) : super( key: key, - validator: validator, onSaved: onSaved, autovalidateMode: AutovalidateMode.onUserInteraction, initialValue: answerValues, From 58c1aebd54649be589fb98909c77f46fd25f0482 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Fri, 21 May 2021 09:23:53 +0300 Subject: [PATCH 38/59] Remove card widget from free text answers --- lib/widgets/feedback_question.dart | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index 666271791..fb71f4585 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -124,22 +124,20 @@ class FeedbackQuestionForm extends StatelessWidget { fontSize: 18, ), ), - const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - key: const Key('FeedbackText'), - onSaved: (value) { - question.answer = value; - }, - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], - ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + TextFormField( + key: const Key('FeedbackText'), + onSaved: (value) { + question.answer = value; + }, + keyboardType: TextInputType.multiline, + maxLines: null, + ), + ], ), ), const SizedBox(height: 24), From 5bf23136bc9c56cbede0f922200b1fdf7f0ae235 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Fri, 21 May 2021 12:10:02 +0300 Subject: [PATCH 39/59] Replace text form field with slider --- ...estion_input.dart => question_slider.dart} | 4 +- .../service/feedback_provider.dart | 4 +- lib/widgets/feedback_question.dart | 73 +++++++++---------- test/integration_test.dart | 4 +- 4 files changed, 41 insertions(+), 44 deletions(-) rename lib/pages/class_feedback/model/questions/{question_input.dart => question_slider.dart} (77%) diff --git a/lib/pages/class_feedback/model/questions/question_input.dart b/lib/pages/class_feedback/model/questions/question_slider.dart similarity index 77% rename from lib/pages/class_feedback/model/questions/question_input.dart rename to lib/pages/class_feedback/model/questions/question_slider.dart index 3bbbb1609..634d4f335 100644 --- a/lib/pages/class_feedback/model/questions/question_input.dart +++ b/lib/pages/class_feedback/model/questions/question_slider.dart @@ -1,7 +1,7 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; -class FeedbackQuestionInput extends FeedbackQuestion { - FeedbackQuestionInput({ +class FeedbackQuestionSlider extends FeedbackQuestion { + FeedbackQuestionSlider({ String question, String category, String id, diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 87d809dd8..f1e72e08e 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -1,7 +1,7 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; @@ -49,7 +49,7 @@ extension FeedbackQuestionExtension on FeedbackQuestion { id: id, ); } else if (json['type'] == 'input') { - return FeedbackQuestionInput( + return FeedbackQuestionSlider( category: json['category'], question: json['question'][LocaleProvider.localeString], id: id, diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index fb71f4585..7d928f3aa 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -1,15 +1,14 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:validators/validators.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -class FeedbackQuestionForm extends StatelessWidget { +class FeedbackQuestionForm extends StatefulWidget { const FeedbackQuestionForm({ this.question, this.answerValues, @@ -20,65 +19,63 @@ class FeedbackQuestionForm extends StatelessWidget { final List> answerValues; final GlobalKey formKey; + @override + _FeedbackQuestionFormState createState() => _FeedbackQuestionFormState(); +} + +class _FeedbackQuestionFormState extends State { @override Widget build(BuildContext context) { - if (question is FeedbackQuestionInput) { + if (widget.question is FeedbackQuestionSlider) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - question.question, + widget.question.question, style: const TextStyle( fontSize: 18, ), ), - TextFormField( - key: const Key('FeedbackInput'), - decoration: InputDecoration( - labelText: S.current.labelAnswer, - prefixIcon: const Icon(Icons.question_answer_outlined), - ), - onSaved: (value) { - question.answer = value; - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { - if (value?.isEmpty ?? true) { - return S.current.errorAnswerCannotBeEmpty; - } - if (!isNumeric(value) || - int.parse(value) < 0 || - int.parse(value) > 10) { - return S.current.errorAnswerIncorrect; - } - return null; + Slider.adaptive( + value: widget.question.answer != null + ? double.parse(widget.question.answer) + : 0, + onChanged: (newRating) { + setState(() { + widget.question.answer = newRating.toString(); + }); }, + max: 10, + divisions: 10, + label: widget.question.answer, + activeColor: Theme.of(context).accentColor, ), const SizedBox(height: 10), ], ); - } else if (question is FeedbackQuestionRating) { + } else if (widget.question is FeedbackQuestionRating) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ EmojiFormField( - question: question.question, + question: widget.question.question, onSaved: (value) { - question.answer = value.keys - .firstWhere((element) => value[element] == true) + widget.question.answer = value.keys + .firstWhere((element) => value[element] == true, + orElse: () => -1) .toString(); }, - answerValues: answerValues[int.parse(question.id)], + answerValues: widget.answerValues[int.parse(widget.question.id)], ), const SizedBox(height: 10), ], ); - } else if (question is FeedbackQuestionDropdown) { + } else if (widget.question is FeedbackQuestionDropdown) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - question.question, + widget.question.question, style: const TextStyle( fontSize: 18, ), @@ -90,9 +87,9 @@ class FeedbackQuestionForm extends StatelessWidget { prefixIcon: const Icon(Icons.list_outlined), ), onSaved: (value) { - question.answer = value; + widget.question.answer = value; }, - items: (question as FeedbackQuestionDropdown) + items: (widget.question as FeedbackQuestionDropdown) .options .map( (type) => DropdownMenuItem( @@ -102,7 +99,7 @@ class FeedbackQuestionForm extends StatelessWidget { ) .toList(), onChanged: (selection) { - formKey.currentState.validate(); + widget.formKey.currentState.validate(); }, validator: (selection) { if (selection == null) { @@ -114,12 +111,12 @@ class FeedbackQuestionForm extends StatelessWidget { const SizedBox(height: 10), ], ); - } else if (question is FeedbackQuestionText) { + } else if (widget.question is FeedbackQuestionText) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - question.question, + widget.question.question, style: const TextStyle( fontSize: 18, ), @@ -132,7 +129,7 @@ class FeedbackQuestionForm extends StatelessWidget { TextFormField( key: const Key('FeedbackText'), onSaved: (value) { - question.answer = value; + widget.question.answer = value; }, keyboardType: TextInputType.multiline, maxLines: null, diff --git a/test/integration_test.dart b/test/integration_test.dart index 2305598ae..3aa7ca9c9 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -3,7 +3,7 @@ import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; import 'package:acs_upb_mobile/main.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_input.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; @@ -432,7 +432,7 @@ Future main() async { question: 'What are the positive aspects of this class?', id: '2', ), - '3': FeedbackQuestionInput( + '3': FeedbackQuestionSlider( category: 'homework', question: 'Estimate the average number of hours per week devoted to solving homework.', From 9cae32efffce22b4dca1377b0ddc7f87d8f751b6 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Fri, 21 May 2021 13:06:12 +0300 Subject: [PATCH 40/59] Modify feedback page tests --- lib/widgets/feedback_question.dart | 1 + test/integration_test.dart | 13 +++---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index 7d928f3aa..d961f6061 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -37,6 +37,7 @@ class _FeedbackQuestionFormState extends State { ), ), Slider.adaptive( + key: const Key('FeedbackSlider'), value: widget.question.answer != null ? double.parse(widget.question.answer) : 0, diff --git a/test/integration_test.dart b/test/integration_test.dart index 3aa7ca9c9..aa39019ad 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1497,7 +1497,7 @@ Future main() async { await tester.tap(find.text('John Doe')); await tester.pumpAndSettle(); - expect(find.byType(Card), findsNWidgets(5)); + expect(find.byType(Card), findsNWidgets(4)); expect(find.byType(FeedbackQuestionForm), findsNWidgets(4)); expect( find.text( @@ -1512,15 +1512,10 @@ Future main() async { expect(find.text('What are the positive aspects of this class?'), findsOneWidget); - await tester.enterText(find.byKey(const Key('FeedbackInput')), '2'); + await tester.drag( + find.byKey(const Key('FeedbackSlider')), const Offset(2, 0)); await tester.pumpAndSettle(); - expect(find.text('2'), findsOneWidget); - - await tester.tap(find.text('Send')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - expect(find.text('Answer cannot be empty.'), findsOneWidget); - await tester.tap(find.byIcon(Icons.sentiment_very_satisfied)); await tester.pumpAndSettle(); @@ -1538,8 +1533,6 @@ Future main() async { expect(find.text('You need to select your assistant for this class.'), findsNothing); - expect( - find.text('You need to select at least one option.'), findsNothing); expect(find.text('Answer cannot be empty.'), findsNothing); expect(find.byType(ClassView), findsOneWidget); From 9a433fefd530c12a2b1fd5ba30f199cea327109c Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 22 May 2021 14:59:11 +0300 Subject: [PATCH 41/59] Improve slider responses range --- lib/widgets/feedback_question.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index d961f6061..a993106ac 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -40,14 +40,15 @@ class _FeedbackQuestionFormState extends State { key: const Key('FeedbackSlider'), value: widget.question.answer != null ? double.parse(widget.question.answer) - : 0, + : 5, onChanged: (newRating) { setState(() { widget.question.answer = newRating.toString(); }); }, + min: 1, max: 10, - divisions: 10, + divisions: 9, label: widget.question.answer, activeColor: Theme.of(context).accentColor, ), From 34875a139f72fb4a34a1ba395a4c5103bf2cf8a5 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 22 May 2021 16:06:10 +0300 Subject: [PATCH 42/59] Make teacher field editable --- .../model/class_feedback_answer.dart | 4 +-- .../service/feedback_provider.dart | 2 +- .../view/class_feedback_view.dart | 29 ++++++++++++------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/pages/class_feedback/model/class_feedback_answer.dart b/lib/pages/class_feedback/model/class_feedback_answer.dart index 97a664d9d..b68e61243 100644 --- a/lib/pages/class_feedback/model/class_feedback_answer.dart +++ b/lib/pages/class_feedback/model/class_feedback_answer.dart @@ -4,14 +4,14 @@ class FeedbackQuestionAnswer { FeedbackQuestionAnswer({ this.questionAnswer, this.className, - this.teacherName, + this.teacher, this.assistant, this.questionNumber, }); String questionAnswer; final String className; - final String teacherName; + final Person teacher; final Person assistant; final String questionNumber; } diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index f1e72e08e..50616b7c4 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -17,7 +17,7 @@ extension ClassFeedbackAnswerExtension on FeedbackQuestionAnswer { if (questionAnswer != null) data['answer'] = questionAnswer; data['dateSubmitted'] = Timestamp.now(); data['class'] = className; - data['teacher'] = teacherName; + data['teacher'] = teacher.name; data['assistant'] = assistant.name; return data; diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 7cc3ac481..7e5ed1326 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -31,6 +31,7 @@ class _ClassFeedbackViewState extends State { TextEditingController classController; bool agreedToResponsibilities; + Person selectedTeacher; String selectedTeacherName; Person selectedAssistant; List classTeachers = []; @@ -53,6 +54,10 @@ class _ClassFeedbackViewState extends State { .fetchCategories() .then((categories) => setState(() => feedbackCategories = categories)); + Provider.of(context, listen: false) + .mostRecentLecturer(widget.classHeader.id) + .then((value) => selectedTeacherName = value); + fetchFeedbackQuestions(); } @@ -89,18 +94,20 @@ class _ClassFeedbackViewState extends State { final personProvider = Provider.of(context); return FutureBuilder( - future: personProvider.mostRecentLecturer(widget.classHeader.id), + future: personProvider.fetchPerson(selectedTeacherName), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - final lecturerName = snapshot.data; - selectedTeacherName = lecturerName; - return TextFormField( - enabled: false, - controller: TextEditingController(text: lecturerName ?? '-'), - decoration: InputDecoration( - labelText: S.current.labelLecturer, - prefixIcon: const Icon(Icons.person_outline), - ), + final lecturer = snapshot.data; + selectedTeacher = lecturer; + return AutocompletePerson( + key: const Key('AutocompleteLecturer'), + labelText: S.current.labelLecturer, + formKey: formKey, + onSaved: (value) => selectedTeacher = value, + classTeachers: classTeachers, + personDisplayed: selectedTeacherName == null + ? Person(name: '-') + : selectedTeacher, ); } else { return const Center(child: CircularProgressIndicator()); @@ -246,7 +253,7 @@ class _ClassFeedbackViewState extends State { final response = FeedbackQuestionAnswer( assistant: selectedAssistant, - teacherName: selectedTeacherName, + teacher: selectedTeacher, className: classController.text, questionNumber: i.toString(), questionAnswer: feedbackQuestions[i.toString()].answer, From 350b0bdb497192e03afbd982f221c1feeff351e5 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 22 May 2021 16:41:45 +0300 Subject: [PATCH 43/59] Fix failing tests --- lib/generated/l10n.dart | 12 ++++++------ test/integration_test.dart | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 73bc1c911..15dc010ea 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -14,22 +14,22 @@ import 'intl/messages_all.dart'; class S { S(); - + static S current; - + static const AppLocalizationDelegate delegate = - AppLocalizationDelegate(); + AppLocalizationDelegate(); static Future load(Locale locale) { final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); - final localeName = Intl.canonicalizedLocale(name); + final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; S.current = S(); - + return S.current; }); - } + } static S of(BuildContext context) { return Localizations.of(context, S); diff --git a/test/integration_test.dart b/test/integration_test.dart index 3080b5ce8..5292926ea 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1461,6 +1461,8 @@ Future main() async { when(mockAuthProvider.isAuthenticated).thenReturn(true); when(mockAuthProvider.isAnonymous).thenReturn(false); when(mockAuthProvider.uid).thenReturn('0'); + when(mockPersonProvider.fetchPerson(any)) + .thenAnswer((_) => Future.value(Person(name: 'John Doe'))); }); for (final size in screenSizes) { @@ -1496,7 +1498,7 @@ Future main() async { await tester.enterText( find.byKey(const Key('AutocompleteAssistant')), 'John'); await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe')); + await tester.tap(find.text('John Doe').last); await tester.pumpAndSettle(); expect(find.byType(Card), findsNWidgets(4)); From debb7bca7dc91ec771910a67d5eaf53cbd5108df Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 22 May 2021 20:46:04 +0300 Subject: [PATCH 44/59] Implement remote config functionality --- lib/main.dart | 3 ++ .../class_feedback/service/remote_config.dart | 36 +++++++++++++++++++ lib/pages/classes/service/class_provider.dart | 12 +++++++ lib/pages/classes/view/class_view.dart | 7 ++-- lib/pages/classes/view/classes_page.dart | 15 +++++++- .../timetable/view/events/event_view.dart | 15 +++++++- pubspec.lock | 7 ++++ pubspec.yaml | 1 + 8 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 lib/pages/class_feedback/service/remote_config.dart diff --git a/lib/main.dart b/lib/main.dart index 1a5abbb19..c0df76ae1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/navigation/bottom_navigation_bar.dart'; import 'package:acs_upb_mobile/navigation/routes.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; @@ -61,6 +62,8 @@ Future main() async { Utils.packageInfo = await PackageInfo.fromPlatform(); await Firebase.initializeApp(); + final remoteConfigService = await RemoteConfigService.getInstance(); + await remoteConfigService.initialise(); final authProvider = AuthProvider(); final classProvider = ClassProvider(); diff --git a/lib/pages/class_feedback/service/remote_config.dart b/lib/pages/class_feedback/service/remote_config.dart new file mode 100644 index 000000000..0559eb1e4 --- /dev/null +++ b/lib/pages/class_feedback/service/remote_config.dart @@ -0,0 +1,36 @@ +import 'package:firebase_remote_config/firebase_remote_config.dart'; + +const String _feedbackEnabled = 'feedback_enabled'; + +class RemoteConfigService { + RemoteConfigService({RemoteConfig remoteConfig}) + : _remoteConfig = remoteConfig; + final RemoteConfig _remoteConfig; + final defaults = {_feedbackEnabled: false}; + static RemoteConfigService _instance; + + static Future getInstance() async { + return _instance ??= RemoteConfigService( + remoteConfig: await RemoteConfig.instance, + ); + } + + bool get feedbackEnabled => _remoteConfig.getBool(_feedbackEnabled); + + Future initialise() async { + try { + await _remoteConfig.setDefaults(defaults); + await _fetchAndActivate(); + } on FetchThrottledException catch (e) { + print('Remote config fetch throttled: $e'); + } catch (e) { + print( + 'Unable to fetch remote config. Cached or default values will be used.'); + } + } + + Future _fetchAndActivate() async { + await _remoteConfig.fetch(); + await _remoteConfig.activateFetched(); + } +} diff --git a/lib/pages/classes/service/class_provider.dart b/lib/pages/classes/service/class_provider.dart index 239b7d8c4..40df9f4df 100644 --- a/lib/pages/classes/service/class_provider.dart +++ b/lib/pages/classes/service/class_provider.dart @@ -1,5 +1,6 @@ import 'package:acs_upb_mobile/authentication/model/user.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; @@ -287,4 +288,15 @@ class ClassProvider with ChangeNotifier { return false; } } + + Future getRemoteConfig() async { + try { + final remoteConfig = await RemoteConfigService.getInstance(); + return remoteConfig; + } catch (e) { + AppToast.show(S.current.errorSomethingWentWrong); + return null; + } + } + } diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 1bff0ce9c..2a6c62252 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -1,6 +1,7 @@ import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; @@ -24,9 +25,11 @@ import 'package:positioned_tap_detector/positioned_tap_detector.dart'; import 'package:provider/provider.dart'; class ClassView extends StatefulWidget { - const ClassView({Key key, this.classHeader}) : super(key: key); + const ClassView({Key key, this.classHeader, this.remoteConfigService}) + : super(key: key); final ClassHeader classHeader; + final RemoteConfigService remoteConfigService; @override _ClassViewState createState() => _ClassViewState(); @@ -59,7 +62,7 @@ class _ClassViewState extends State { return AppScaffold( title: Text(S.current.navigationClassInfo), actions: [ - if (kReleaseMode == false) + if (widget.remoteConfigService.feedbackEnabled) AppScaffoldAction( icon: Icons.rate_review_outlined, onPressed: () { diff --git a/lib/pages/classes/view/classes_page.dart b/lib/pages/classes/view/classes_page.dart index e0411e6e9..69d911329 100644 --- a/lib/pages/classes/view/classes_page.dart +++ b/lib/pages/classes/view/classes_page.dart @@ -99,7 +99,20 @@ class _ClassesPageState extends State { .push(MaterialPageRoute( builder: (context) => ChangeNotifierProvider.value( value: classProvider, - child: ClassView(classHeader: classHeader), + child: FutureBuilder( + future: classProvider.getRemoteConfig(), + builder: (context, snap) { + if (snap.hasData) { + return ClassView( + classHeader: classHeader, + remoteConfigService: snap.data, + ); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + ), ), )), ) diff --git a/lib/pages/timetable/view/events/event_view.dart b/lib/pages/timetable/view/events/event_view.dart index cfab1492f..6ad4926eb 100644 --- a/lib/pages/timetable/view/events/event_view.dart +++ b/lib/pages/timetable/view/events/event_view.dart @@ -119,7 +119,20 @@ class _EventViewState extends State { .push(MaterialPageRoute( builder: (context) => ChangeNotifierProvider.value( value: Provider.of(context), - child: ClassView(classHeader: mainEvent.classHeader), + child: FutureBuilder( + future: + Provider.of(context).getRemoteConfig(), + builder: (context, snap) { + if (snap.hasData) { + return ClassView( + classHeader: mainEvent.classHeader, + remoteConfigService: snap.data, + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), ), )), ), diff --git a/pubspec.lock b/pubspec.lock index 95d532735..7e6b01e89 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -344,6 +344,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1+1" + firebase_remote_config: + dependency: "direct main" + description: + name: firebase_remote_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" firebase_storage: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7f30cf9d0..177ca5ee9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: firebase_analytics: ^5.0.2 firebase_auth: ^0.18.3+1 firebase_core: ^0.5.2+1 + firebase_remote_config: ^0.4.3 firebase_storage: ^5.1.0 # Flutter SDK From 90874ddab1a17b1e278e60347103d2a509d069bc Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 22 May 2021 21:18:03 +0300 Subject: [PATCH 45/59] Fix failing tests --- lib/pages/class_feedback/service/remote_config.dart | 4 +++- test/integration_test.dart | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pages/class_feedback/service/remote_config.dart b/lib/pages/class_feedback/service/remote_config.dart index 0559eb1e4..032af0fd1 100644 --- a/lib/pages/class_feedback/service/remote_config.dart +++ b/lib/pages/class_feedback/service/remote_config.dart @@ -15,7 +15,9 @@ class RemoteConfigService { ); } - bool get feedbackEnabled => _remoteConfig.getBool(_feedbackEnabled); + bool get feedbackEnabled => + // ignore: avoid_bool_literals_in_conditional_expressions + _remoteConfig == null ? true : _remoteConfig.getBool(_feedbackEnabled); Future initialise() async { try { diff --git a/test/integration_test.dart b/test/integration_test.dart index 5292926ea..254d0222c 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -7,6 +7,7 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_sli import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; @@ -378,6 +379,8 @@ Future main() async { ), )); + when(mockClassProvider.getRemoteConfig()) + .thenAnswer((_) => Future.value(RemoteConfigService())); mockPersonProvider = MockPersonProvider(); // ignore: invalid_use_of_protected_member when(mockPersonProvider.hasListeners).thenReturn(false); From 741db0a2512e3d464ea74b2608f731e1daee4196 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 22 May 2021 21:23:57 +0300 Subject: [PATCH 46/59] Fix formatting --- lib/pages/classes/service/class_provider.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/classes/service/class_provider.dart b/lib/pages/classes/service/class_provider.dart index 40df9f4df..ffadbbad9 100644 --- a/lib/pages/classes/service/class_provider.dart +++ b/lib/pages/classes/service/class_provider.dart @@ -298,5 +298,4 @@ class ClassProvider with ChangeNotifier { return null; } } - } From 6a63fa255913b3f7f65e7e1f6b265c693c528791 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Tue, 1 Jun 2021 21:37:13 +0300 Subject: [PATCH 47/59] Change question type from input to slider --- lib/pages/class_feedback/service/feedback_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 50616b7c4..2f9e63f5a 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -48,7 +48,7 @@ extension FeedbackQuestionExtension on FeedbackQuestion { question: json['question'][LocaleProvider.localeString], id: id, ); - } else if (json['type'] == 'input') { + } else if (json['type'] == 'slider') { return FeedbackQuestionSlider( category: json['category'], question: json['question'][LocaleProvider.localeString], From 2a71f4c55f136c3b4298b5b123dc8da661affafc Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Wed, 2 Jun 2021 11:05:20 +0300 Subject: [PATCH 48/59] Create feedback motivation page --- assets/illustrations/undraw_review.png | Bin 0 -> 24062 bytes lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ro.dart | 1 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ro.arb | 1 + .../view/class_feedback_view.dart | 8 ++ .../view/feedback_motivation.dart | 95 ++++++++++++++++++ lib/pages/classes/view/class_view.dart | 2 +- 9 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 assets/illustrations/undraw_review.png create mode 100644 lib/pages/class_feedback/view/feedback_motivation.dart diff --git a/assets/illustrations/undraw_review.png b/assets/illustrations/undraw_review.png new file mode 100644 index 0000000000000000000000000000000000000000..44c4cb11eb479756a5b5c7f6e22a90f08a73295c GIT binary patch literal 24062 zcmb@ucT|+k(l0uQf+(PXAVX#tK(dl^9FUCU3@QUi&N&MbBxlKjNDh)EqaX}92gyp# zk{RNC27LF~`+VPb?mcT=_=C0j=}=wORn=YfD?(l~(FqC-0RCXSmU|8ZU7>zIHs;2HK>8IjFfldP$;}xoSG7^(iQN~Ss%^Qf)$*~RVKe1SeGN2`*$0e z(&{Ohl5JI+DL%j0!`osLoX*Rx{uWF9DGn7<2b&lmTe=K-PA!uC$hC>PZ>jELw&xny zP{C|)L4ES#M+NQE8(134i?_Yp+&)Fl_klp0vWKb$HUmr*5GASDXp9&j|v|DOc+4jiNL+- zj-9=5@XVF!?%SuFvH-V$?QPLRIEXocqe0Bi#2$IgnHk)*r;nf7UiV5Ex@O8$23}I% zM?atmjE9d0>`5`tX=AJw%!?ZMm`S6ESpJ&15mp*-Qh!X~Bsup&@X$acTLA_6`JKPW z@Bjk8eiEyepoCp_+y<=)K9{DiOYs8;1U3{&hAR+pvpUwsc{NJA&#nf<8o};4T zV*Lj9!}YX1Q~v-(fNhmzg&M^wKHD)O8HP=s$j<9Syrme`=a}6_f$_~fat^9uU3l?f;WdjAiGtsrRBPpmh81utbL^5{QejK!%bk)FhFSpGRKN_ zS4`$n1NDlu%3eRcS+rU^+6tQjpk{(A6m}cX8iInH9J(L}7>d9MjGwq(4{wewdiFjU zb=s;m2LAPigzR}Yd2d#yFEVUy3Tn`J!9c#Q=p_CW{XTF+s!c`JJQ#L2%Z)3cqA)-V~bo`lDRDG zlhCN|z71xBbJxDwnyk8Wb1;ZIE$&Q#VJLlz3ERBs-XY4bNO`So5}yKYl*b#w?k|Q8 z=zL}cWdCMMTT!?UXy_=NlrUnrk;(-)dJUTdkktn~>KiT9sj*H=@k0%?D|`Lp{kNM% zCrl)(ug%5Fapu2jpPZab)`y28P=Ew3a3FTm5jU#OA;2-K9xQ#cA;hmQ3pcB$eeu3H zrP$LE`|Ow}FpBjJn;BSkG968M>vLO0cLIw*$X{#xeeL7Clky$`F~}* zKLH2wKm7v87l&l)f!e3EG;-ebdNhzN<5~x<=0#H5$}|qhRti$C!ZmGR^CS}W$8H0X?xI-?nNl~YmqcI{8c z$r~Wa0iMYzTv_xkw?Sl(4szz0b`P83p5SHNqy8=uCr%d(&`Zr^B3^cS`ql1TjK{FE zQjAg_;>A9YZ>wvg-)plr7Z|Ana0K4ks(8jM;NDs7MQh2lcL++2TENmvS->fJ&$;es z3Fv5m*8)e#C_ay?px@4LC^~Sd`Tym|cmMV+znzl`bb3=0MZo}IqoAnU_ZWF7HPy6v zs`A)9O_kMWDyfwMm87fLiPXoJq>y_E3u$1~WcbDh%I`O|mb^BjjdS}IU z)1&V$nLjOY6K_4HY{0b7@;_GC^Qjl85A&l9H^!2m-D$2zm&h{q48NIFTlEiI*Iz3{ zmOiocqrF_Ib60Mm<}g|M8=*00@J0e}m8JQsI_&SHQ0JwkaN*y+&D*S3UZEs;XVZT2 zxe0^71x{}vIdOjc=TE-)W-X%B*mE zzHuBlZDUdn27h%-nW`gfwU*PGjh#`g+FpvuS|TtTvJ^WWmm_IiRrgWt|%8Csjw?S~Y`ZONFyK z_}v_FPn;I1W(Yj*k&{gybEGKR(xBKPw@rULryja?f6EFYs^>kue zB!t~m8jVd$ZF_{QMQ6rg{*GQR)Olwz`~UnR?*2MpX58INGRRRUH zX76~1hjkSyL6(>$P6-}agEb>R@Z zhi7iHF~RbtAs%U&&p>%?-_`1+##03j~IrQ$jS|;E65#sL9l{m zzfCahy4Q7!=*Pqjg2CxK4w`5mamZai=Q6zQV={7e!t6TE8FucE>DQZxHeIdM+=S0x z*gO6as!sdnX~6jkS$b-f^QmhUhc&oKhr4Pko7q;eGK{Ozz!$Ic`7B-xr+j=;_+gr~ z;A{vnD#HLGMi|NQ>&cpD|;-5|@$1aCSK1UH}c` zoUgky8|rC`=#4q*8K`NyNXk)Tp3Drj|8m*-A;8|!TvcW9TpZt*T5fjZl?M2;E*phwDr+{)4l#K<`(M;7?esKVn3M z3W){^C5Uw++MQSi;TAf~;|Zmt)P~JGr(Tqmc3W364IgpcEv>9@1_)(tvFqXc+rGt* zO89LU6BX9o(<2y=i$;~7H*Wo&u{FcV-qzf4lhZwsj*FCL{M?6|A>+MfpdeWI+W=H9 z20>i?TS0A;v7m_3Mhkz)WtD+kT2wej3!0mIyoKohQ!8P%S1=sA*EnT#&Ns!yGSZ#L zZm!c=#y8tiK z`z_}#l{zt5H7XyujGJ0qeP*?RP4tn;w)Wgx+qY?yTLy7i#!bV;lahP8WpGhF3)7NE zT2q-k-57=pWc6Nway_Xw0?QTD5bwE1>TR$&GiUS+3F04v^oMaH(#e|Yt60nlSU4(H zJ@(w3^Ze?bW#s>+svT22T#u6#-FzWcwGBP3)A3I(&io7;mlO9K&br^>MEcx+l8Bc> zRUn!kqa_;N+u~3y4=ec2Mxr8&EN)Qf&V9^!Z-pc_c!aIo3~c!9jVPHLVVB~SHQagm z5K-NX(UAT*Cg}}$u0|a!^ld!Vz5U+9pzZQ_>vz2bQRoX2+O77~q{<*$GBES3&9K;} zgjAdoOKJowv}P_0k>%oJ4g)O7`gYYCN2<|)ud6P6fQ>$L>epPDtndhSI6t#;Z$z8V z4@4Nk)ci;czjSUQGtAs7*fYIL~-_Dpb!UhF4Ge$Eua8XJ&(YHe$aKlEs7NY%^V zH5o)Ef$?J{k6^%x-S2n0!@NOtz^rta7~^LOvW>-w;uBbvr&cPLy60tAM8#x~G9E>iaEq1Qwy?P(? zrGBn+Gew|Z{0=#$#p6#gC8jiNe#M{=nn&9d+(-7IeFFz4JY6BndzPq_XO-&$%$l6LM_9@BWB&07Il@v<^qER~e{WN+Z(6Fm`DJEIaFJXcbTMUO+kVO2bqGX8vzM&o zbmHHJZLlF6O`L?-%OB6eZ?Im}=1?TH3AIeqP5o+0d^v*9ESvmfU^FzH2|n}o*um-J z{$*Rby;m@@)$83JyT?DV8^ZXB3~^$V6t7inL)1>tFqE22&q-Y;RDR000o^_2Vxg+T zcYC+i1yvMJ{w?Qp7otS!YlLl}a%J>$@>o3<^9z^M7TIWiKp~@NQ=2ATf=!J(c~MG< zOHGz%xz;&cn{>atv-I^Sk(%{oE#Xq^&ybJU=^_!hSvF5kCRLM7^x=W8V+fa2JL7>H zy7gS$Bb!xEn$l9D%fhre2z6-yxOVxD?c1K7%Z|Kan|e{mkn6HE;Ki#??#vVKm0TUe zsp`LFi*4q>8s3k3v#aoYYx|jQrjf|@w0YaM>Zm=N-MX{%a$u-ff2r!T_u#jKT&a?& zD9!lUkdk=f*M}T&+M&M&vsd+LF!9d%#CHQ&v#KOCm}B>e-(ZdLIg=p^?PKlBbNIuV$`67(E(k0PgZ=x5vT+Ky z`x_-D5^CU4F@Gt&7gif!p}5_`^Ye2Fv$cUy#AH+vYjOY#GP6{uAOm(49aDJ5eaQ!t{_!tonvwa^CRRr;5i{TzhqAeFwIf&$TYjNnEPl z$Cb?@Ho6YYNtKEo5DpDx7DoG>u;}yQDN}b&FalxBYOXxSb0*ct{N4H6WAr+Q4hniS zdzDK|x_@T-8M$d5}*C+Dr&8;&yqcd&MS)xfcKH?d{YuyvhTx#yibqub4V@ z8&?;S5W#NA= zaIp~H7E)t{TVeTNkfm7KPIT08?PF`9neSq!5?InOii!eH9&Ib_bXmWbdViA*r_SHZDG}=Q@sQg)xuX(~DS{X6`s;#3OkH`ns`3bqqN3)px4-k#vbnkXw6J%k#>Vp<*cUg1=UimnjXaBS z#Vjon#AulmrcQLKD&o#J$4Lyk@8MbsFx*dV+}VuHvBjLTuqkWo;P^gzP-VFg%9;=? zXfpz8QgSab7^rzcIqvfzJ$l*2ojuEYcPNeIR?BY~^xPBEpg%uWMD9Lg%>iQ(Ze;Y; zg(Bt{af78K5<_$Bcx&GG?AP~qzc#PnvRTX2*l`q#r*>n8GcqaiqBVeZLVG>X0^hFMbGY?5<**h z5R`e;pdtm>Yl`48A+E*FKi(@mae`HATHuMkNV|viqwn{)aI1|@Jq6$_GV?7vY(*t( z@13MRe_<5tOg1ILwCW*4NzN)sa`5{&u-{ ztozA-_km~n!2CtCNEFRgV~N>Ewo=18I^b-PQC^y|tK+H6u)!b8RSFz}$vl7Rmt~St z?};e0RNG9>eyzH5VShnSN>KF@i-of8eb^(zWb8W9Ns_3H=u3qmyC3t)rx{Z66Krd5 z7+4VxXuB`l^wXmC&gL9!AIv$1oura-5xYxkc{V7Mqn>h)65G%n9f2f>jwx+!J5uD6<_e?}h;Zg?`-+0~eo>cdRcW}g|Y zJZss8`yCmxuU_imG;j>tA68Ugt{I&@7-eH4$)9_sXTsk#smQN$@6b!8+yri;liUuL z02Rr?)&r}mrs5m2;z+uk?lIO}>NJ_^7gpREb^652@DaOdig(o}vVq;%U+4MItT7|w zfq&K%*^c#+j!nBgxkLJ^i+OJRV4v~`xh z;ws~#fx`RCnwglWbKwN12kfLO6hbmW><$l+SK$>{Hn4FeL5tC52_%vYW?1X4-n$1g zk!kpboGU`d>b2TAa8Y0q>e;}W8QwfBrsd-kGgR!ejXb}S9Mn#%nsH?#(;x9SqMNyT zv9^dyPhR})EDWtPCd_lu-pxk}2koUg`WwNC;C^U2YYW71Lo2;ESuDi5=dWQ*fuk#hQUzRmKi9p}*mz^kLOv#BSWAa~J!ekcLEGtX9vpI+VlWv^c^I~fJ-nm?F zla_jW`<;E@t4&z{NRc#Vsd`a0n)NT8DNdG=P@)I=qdnye6=AaCYN;s#(0p9Mp1wZe zRGnKbUEkdEbr5j>7qgvx zKeGgHrnq3BASntR2X)Uu>KPf+S$H;tC@;utIT4I-gw+TWi}ku9KpxrHIl0{=LE7fe z+)Z7+stCO4JdW(z4WxK69{V$pU4ZN$*8nfp%F{;e{>GBSC?dn9`lwF(%$UqBYZ+$- zI_{=TYiD0PHJatB!|!NZA=^#)j2*Am1hER8n$)b1)Byr8AY%Ekx6?Lo2)oxNw9%zb zq!U+gq+LZ76;MR)WlcNBq#?i~l-&Lf7x-hF+mI74ZUuw|LD+JzuI_L}SPl+JdRT60 zsny0qAXWqdkw3NAjMMu>5G@i;oDmihoSv}G_Ng891>=K|Nf%a(;3gM8c=xDNt_}vn zDZk*8S7?7aOGyCd)(r>4NUg*+h%#^3Q1b9 zp4QXpOibcabK_U1A9{LvB=W$hwL%g!)W`%2E%&a%9ky2V7Qj$X~2CC*;~6M${~ou*7ivTG&eSuULRc(*{h5{*(k22Byp7pD#Zi~ zNkJh#fLzbui{&R|zz0$0PQ+q}RPTH2-ACHdzMj<2=xKf=whyB=Mh16Yiz^J1hOx4e zMYr5;yGqdUOpjsz%tGPGTAN`-dT^gXYHbA7T zCvG)`@8b*%Hs+4KfQ#byo8g|JPE(mIyGSVAl@%K}DV5UFrlXK+M#~b#?T<7g{P;5R zve31!o*F+Fo4Yb1J9Q#u-QqWrej2MT3-g)`8J2ULjn^axXPdnvN4nWG?6yZ+j1}#U zjg1+6H6Jaw7vhk>s_#y$gwm20ZxxkFO2q4ZQ|Rslxq_&3=i38W8FBF~ubn^~AdC$E z)C{Glkt>Rwpvk0sn4(c?kk*-o+WUZtxIJ8|-Ip>4cx6r*>`{mOcqk*99tAbhydUyh zZuj!^aKpnk5hZ=c;0AlAK|B!iUsY2x6H>!W>L!X7`YM=wGLR-lC7`JHI`{cj#3VP3{er zUo1hfT>nGSozCdRmvUJ#QE#T((=dUyx+h6VU-RNs2YMqCYtODnRyeK=`O{+6&b_^z z?Y6iJ1sZ)>#jVnqWDp|EisEVknk9N}x0FyjleX_pWE0VIF4BSfL$eu{_tj9Hp>+P2 z5>TSaCs<~aUB`4G=Y&ieWWA~=Im85)Y+%ybd(0}fkK>OQ95M+$16rxD5Miyo+)Cof z8PBrDO3A*fqR3563&4)`^YT~4#{(V@fDC`ZMSTjeS6%oRu*`@e_mhc$#(7cp4;f1X z&;U4!9$Q;T`Hx>AcRTN*K0()e9_AL~P=is7B~v8I&q#pEAeveq$VdSVpp@8m>!$SD z5OH=CWfN)Glk~K&AX%@s&yoAhtpq6PTS}Y2wv~opupdS}VW=_Os;a8{M&Kf!`f*}fKW_NQ0xCYP(3pj5pQ_@#VM8lYA5ZNAvo9&9zl z$MeFHgaFRapLHdaFd)GO0^z@0>WZgFgtdg~^P-tiR7gw31muMCKIC9tJ<3S0) zdM=6Y)|8^&v{hE^NI+?DvtlBp7q)QJ8~_wa#8!V(61etVy&oKczIk7Uo&V9XvPCKo zVS|xur?!Alsk(elmlpE1{^@VywWQKnP*@tl-kna#pvPYxd_Ll(!9z-OOG?}Z84e~5mH^7xuDzJ27lzdy2$Jmb%$W1oV7dK|S++6wz ze_8U4-c-Q5ewKL@FX{!M!GOJj`sP?xQ0HRx4I`)s<5 zuT^w)9sV?4TKQjHL@&4vF~$U>Szm|xG@Zm;9$5JdnTDs}%ZO*~9;TLe@{Q2_C4sne zL-Gfd_*p|>)$7kqC*Oh&_z2i6(|tK3A1up%oi`nM^0(szel(^u=VZj^D_7a8vv-=6 z2|%AJ2Ps4M+g0!X?wp)`U^X=m?V`NP+-tNeK0-#+K)V6vbG}acT1GUv!^pkHdM01{ zFJz}!tHMM<2ZDJNsc%;-eAbYEx-dSS&F1z-5#IQYoFFQ`w5fotVS^2`1bt0fPhsmX zBj3EkuFIw6#H&3|w!xXCY+Ph)+)F<@OiVZUINs&_HK7DOKR*O2{XnTMuL4V2l@p<; zv+|<#FYk1yHY6w%7e#$$jmInbe0{RdYszUxDGwi0q_I`tcM~KBG@OjQl~#FVdTU5f zI7QoCjIk>0W^?l(cF>iQxG9YF%xe?f&5dP!cW&+@Te7gK&(hBo`7`-a;j6coBfT}G zNdRLMd@^`c5=59mvp6?~kJXincJCOCO3 zAPUs|O(T=+-|Fq_tHwr`@73qq{blhCgabl=pB60W-Z`3seUPZG?y|hkd5zzwd(0u% z9V)K~gPXwjVCLjEgi_(<7dO)W;(*Z6LQ0v}Mo40=xSJ^}x^sHC_`pba#h;60<`g$w zSBDm>vfZ#U@Q_>TIoNxe%pSEI8K%qLA=0l62HkD?R%L|pyl&{o?AB>?cT)IG5Y43E zx@0E1cpj-(^Z@KLFO;OtUE9+G(4dXHM^rL=jp+(xRlr!7!cPy@v-E`Q{mBj$s1k-L z#$WxaA&6#pkn|yVGe$enPcB2HlNOZ9OVI=tHL77un0J*2-$ zI&GHA##mbN^G{ZJC=^iKy+$p@14XW0(;p({Ksr7*pdq&Sa+=mhxY-lA7rQnLvMAE4 z-7_77LNtkm5k7nER~zz>n4jm~f@&IG2TV|(JQo^7uP7)}vj0w|6fPub z^=XiMf%4D4V~%_SB8@O~*=b&jL&_I3`N%KGw0n1cY}%<_S`Jwvxcjg(2f2;-{~eNq z|J)nxDOB48X5i}R{am>9D!Dm}akfsZ4*((?&dxYSe_432S)N0Km=Tpr9t z!%D*go5(=e57}EW&)I|;KhltzsBtzw*t_syH|2)!IhupRtF@aYwf+s)1oeUJA?_rS z2(S9M?N*S>z!WX&Fo)^&CREAU-v9)!MJuaD<|GWhN#UYlPeXxcZgW7O5L-s1%i}L_ zskQ*vVg?&a=-~>Rh4%xKM8!*gEE&XX(&xu-MxK&b4HxOw^!2u&P*h(5eAJJZr<_!o zHZB3oNq9-GZ6WN|@0-k*y0m;Q5neKg4-E8(V;^&Ih&^UisFVO+v&=;MJTUaFeuXq_ zegFo2qX4lF$wa>jqSt@4(LY=Lsx?p|rHd`-@$74H{JkBdU+9BeXMoa@e@9!nnR)kfoi=SG;TT z65!R~5q7XS8*RFNwdGHl12bP^4oGA18+3|v%gr!EEYGDObE(her~K&$@~N(2o^!bV zN=l{i8x-Qj{}|$q$tnvUGhpLonIVX`0^8Qdrd^v#&Z@P>+>8AFrV>pbxSVD0as71; zR2b}{BAqcXcuZM{^r#`ln;p}pX*Cn4>L6y7tAUB(VS1&{+=Dl$&bHNXo>^XYU{gd_ zR?Z$G71{Ycdxx5&=dxq_*FHqyuYr3uD`-Hu zrU1_iZgiic4u^L6b1bfqe;hRYBZZKT89AYeuW(V9deQ#$x6pG>^e_>7+y?sSef0ZCL9jZ4m|pubbc!oOUG z4HHBIURT9Jz<7x}UUke~@AZ8DG`H9HcS89>e+f^6q92{%X9UZ005ApZ+VamC6-D`B z9CC6GVoM8oto=^2UyYFc^}Hwe(G~=0!9Bt@1+Qd+b!>*ZEQ4$E!|bgQ@1!mo0LSn1 zY3wh~tqqt_+7-h7?JLR{UX*;!P##rl5>ZNX&-2uu_?V%|Q*n@Gl+X2-id}lh9V{QN z?iRoR#uH*17+O6~om6YmUZ7KYwraYSCR8&0JUsK}0`t_@GTA9P))bch^|Jb+gk`C= zBHyxHb&PtCaa161Glt8$i!z_(@5C4Yc+^cOIs{QkIz=1h#en zjSc7aUXztfhjMJS8dozr)&r%;3zpMxp6QY~_wKY)%AK#8%^M|W2WLAT(mwNkNvEw2 z6ya?mm%EP8f&A`xep6AsKq|;c zXMIPsVt3$B6xZuDUEmU@>T|mlp^aa@9-Clb7r$g-e0_7>>dOr?2$6fnPa#EtdAnB-80$HrLRH=^! zx5QlXx48deq+jABcKfJi5k${oG$lJ7Bleq|E~wLg_hqCTpcLyZtWEj9BB-f$;q#aF z-wC1}7ffK*I0;oJ93{+rH5}tpH)=)*g+!G7C-OFm19We7*te|{z)Qd|wle7X@j5x5 z;?+8@t`hl38S>p<`c@Xp@7!n?8>q}A4I`5@V4CQo0$ly~C^#GwFw?x?#!3$Sv;blt zT%;jbKAOS7x|52lXVE1wau*-(dGvoEckv~hOBrh*cziTM<6mHKS?|1F$z2z0%E#SK z>&p?To@89W#>_$x*cdn{0C3;?9ss0>FVQ#i!B`S9MBn-q2A}qIm^|0Mb8&Cv)PsMjJG*@NbrJmP_6GZ;xn@>P8dfT8=QC5u@-{3YbaAMvQtn^RFbIJ6q_sx`91RQ zXi>jmxXF=-6vmoIT6%Dt>%FN9xAn!tH5N9#tN!zrA3>_5z@~q`Dg7^AvBeXduj9iM z^N`Tg>)@H>;^N{@T+OLp2*HC)qNre~lg6lF@8VLNeVvTdV*Km@CKeMB4J)IC#UdnFb6QVKtTf*6n!_oGb&iV_kq(X z$5W* z#;i6H0}5$kG{5uD$frrzO)FYy!LHcJc*^6?e(z`Jt{i2W{B=GbQi6*r`SEs-z;C1> z_EPR8&q9K`kFmM@|C;P+7(D1OMEaZq6{Y3g|RxG8**MjP}?c z@M=v*+@yR;!2f3XaxHYqp^&S`%p9iVj`uRZ5c zmiuefu<1-qrp-cOi#;a<(cPiXlXia!%gPK!Was>)n>ac6qyQLy(kCde4O@IG*4m5o zG`q|ndGy!nq647{1rI1>w?8o&f0GS00nP_J@EX&z9gZC2 zTeZjkjE2LpPcq2?{i)Rk9Ik}m4-+~_sfy7*^e3%*1nWqA+9D?9vEJ0y)vb3uTptx!>c_xJ12p0d zpGL9H=l^(`R1o0JaUIFPerMpV#YS5kt(*@nb!~|CQsb2gt;s)^-F#(>x)0`0Fg^1V z#(DY-FM96J6fGXuR>Ly{5mpvUu&+IQ-u!7fDtKb@zs0%x0+vjL19NMw5mOpl9uR(i z-adXzFrtLX3B~J(nsirL@v^0GPtn*5mz1$yIh5^qGIs1$jZH+^^zwwaLU3rNp zgun4Bqu_AXYid@9hpi8`_#o2`E2U9JX;k)l4k&qU|Qh9Smiy@K|R z-5H0wvbt+C;%q}jl_2{4DLJed$eEsf zjh`i6Z|`0>$1|b-NE5PWs!e0Bv2z*XA=JIp9I69@yzcl90}Rwbsm;CQz*u!VZk#ZO z#E7^wPCBjY&N%?8a+!;_urS+n-0mQI7)DfLqq2$UVLqkc3W2q0{MSm3m$s1Kd-0?~ zygOSIlj|1R3WKHtHIL(!Iv@N<3*h_#c!J*sgE8pT`7%(5b1LD_|MgaRsmYpnMfVAF zd0VEYIw>FzQG0%Q(Ucq66DY`Q67wnqp-)2(wbX#c)F$QugR6niWNa%R1lYgbbAQVq z;b1^;>EIx6^`XvK(|84iHVp^XT&i!0$x|Q;mi+p~+}nWO_g~(m!P*l3+X9l7N*Fxk zmH#fsea9W+JNUXGO8&Ks)EKiSLdJQ#`J*Yr=tUIInA%hEluO&Hqa?uwb}H(@GZk>#!eP! zc}Rgcf<8Mx03b}Qy!@7*yG$t?jw)~hjCe2$9`Z?o-w_yHYU5phgOTg4p-0E^XN>?2 zToU;1Uo&+oDYB&gJ_ITyE-*yK#HyVSC30w~8Mo_BFj@(sErKExEdg0HivH_W6x99s z;tA04YPmyR0cPEJstEMUFDJKsOAsAs(0Jbb(d3^gsabkc@HVLGZGJlC&_NQXkq>~b zFZc3?P? zgyKZsXutQHcK`>E`|L#_xRj@|!E>%&eIYJ2RGYNVxlj zh2{1O0I$#y8`T`1R)krk^>OxhmXi;d>u=2M;~n-@AI5Do7k8h=KWpKEPzczRaJzo{ zg^k@JVWC!R6p&FY%}5hMP{>FxY-qEo{435U3-CkpH@R)D>GImU``n~4!3WL{W_BjM z*zFHP+|VwzqgSeueb1>m6)9;ro`Bh7UnfEXI3)kt zN&45HrkA%zIRD!73xa62h0|~!DWAsq-7AtBi?Q#$<%-$7`7zN4j)o%tgbYR1oSlz; z?c$bK1S)WNp3OEHkJD-ba=M70l)5<;595>8_!wrzssRNul4Zr{^^oY#s29#D-MZ>{ z*LmbV7Ia>pd&B!+S%p!>NS7$Dqn>yxI>LATmLq3rKB4I)&Xq9;3Ws=uKW%~B&AIa- zH8CuB%B7}+FBblCqAtG`yQ<>1BFu!@hAWfzEHnNTP#2#K4|a#Qr!8j&mO^-L=OUen z!CyUna@+rTG^+-IPchmBT8`vv8-KvRyKIpOH6FKcKM?DwJz#TxWjs@(q}gmQHAD-+ zuLp8yH2FYO3{@8B2ZJiy|8bGx?KZ=>vUj#bSmXi$E?_hk2Ni*aBxkRWWAXTw9M;9Tae@t390!6ZVhcKts|IDR2DU zRETuJ&K{KVWQxW4cs+h97KrlUpe|Q+oWN5g)*oZn5b<^=aj`!h-y`kJJ;8^OtDi6Y z2(w_YuMd~3A1HI>jXy97D!i2c?XZop85{h@g&z*Og1Of@TMn-%%5`7FpVy41eF#Pf z87lTVDE?2?L@mN0!8>A@*t1s->-oqaij$wJs%KYvTd<`Tcs4antIxfvel$*-KgWZC zDL;fjpcOhu`n>Ak<)+CthGkrEPt{eSe3OOl)pu-+jj+DR3Uqn zTbNt=|NROEMHd+58rx@DkTOo zL~q|*jm{rp_)mi;nDN~Dw*8Mr)NIUkjibP^m}GoRG20ikTmuZv3VRsuKjY2eS@i(# zy}RFKc4Z<29)08qy1>V?s-3~y+)9(8&}S;2atj^v(o{w#hoX>$eCaJ!D3rPMC&dma z;9@?(BOI&6V&~sMa3b8uyD4per(?5^c2-{&C(&dbfo|BZql5#b=R^~srCe5DkiHl3 zT3yk}1!>!D6_W19dzZ;NK)%kmBr!Pqw0hEW?4@&&v5X6n(q;BnVDZj%?Kji`vL{oH zJ;I^lX|^7Q8$?TUl}QAo?LKbMLbweVS#*yh)?dC>U#rxPK61W;${xnZQawD0xNu1r z>RXpiVpnmfAU}53DA>U;M@`GOp4yn3vJT*QviF{P!M%}{0nQj$>zBl)vOvvuP<&q`p> zQA%4C2n3Ve09K4*)let}=*`M_y!`^xs~=+aX+C`Pc)N{%zsV`}Zy9a)xz!yFyLLb6 z>>9rh$8<6y#H*?0rcZ1yzd+?J0IFpgGlYT_p&hqzyKP>)C8)x*oNZIsBM@u`Wi#zK8>U&gQtI~EO zq*GI=#{GRkD-WUi=$wvPBNtmzSB}BxA9sIMd%Y zian}4!k7yuqr09}K+GmlyeX{#OV6*ce63ZOP5_1>D~FHVKiZ4e9g0dwN2G9MS9U3Q(S! z9%xi2B}vZ3ZZz#g!56X2=|K`pnl`*@7>>F+#My-tjLqs-=?j=^qrWiK$QR2TF7B)RctUp^+bpB~+l9F#B&S~zc`4i9rM%n10QZ-j_e4e8{5&)_lnCE)d zxpVdY@;~U9WHp+>r%$wbp?;#dB_xsGaRJng)p#u0#k}#P-F0$yo#DiReK6`vMg?w6@}Zt5frzs5V$bnzXMH7pn*AGRaPnuHrI4=KDoF=XrmA?L9!XdM6hgP+FYslK=dr|P@Xqe8u zmSC~d1}%)T^+Oi=gi_mAGi*j=*&BPON_7$$zgnLq*kuxQYnHFhWOSL@1m}l^Rv*~u zAWsjOgk|x=*q}!T|5>QU2)3&ax=2m9->Fy~g7ee6l+~j5*rWL_I)PFGn`@N%J+ytd!!le1Qy?ijETVNGA^Bn2=fm3eWvoS7t%+&iF zozy=zDD<;uKPoC8yWX0!*$6e}DQ;cbm<{hs*Q0wbNCA@zgwxiWgjX+xNxNH%8(hU{ zwp^MV1GqKkhS21DFGY}}JKoJ-%e}wy&ll&FZ@*6v$1ty{v^&xF*p0ycV`H^7869=| zB?@ByftxqpO2_!WDKUf8b5rVT#8(w`)+PD*IlDc-V@=AgJ}YM z<;w%d9k!*FtuiEkbnp6O#t{t=-g*_O8`n~WBCcY1M4HsjP_~KDV)9i#-ID5VR}_6~ zptCGvNZ2c;S=bL&?Tlx$6EI}!C)yDAKJNZ6{0(}v-cs>O!?F>*yBHcYeAO=w%O`roecY_txFY8Drf=q7MJE5(60$`A^b0l|;`rS*RGZb#e7dn(QEKs_lsOZq>d6_}8O|R_v)w~%I)qp#^O{Qlk_-qNhv$p0Dtp+Mz* z^O+FObbWL3_oy1O0M+c=Q>jVYfeWpy9sh>QgUVi0$uSjOG{=h34c4=c(7-q)Zzh}J zy|s6t-)q~Uki$?*opw!ZhK$tRSL3EyFKUfnHY}-{`oc^pXKPfas4`Zn+O2%h^DEx4t%s3J-Gx(#D4J zrf~nr*dsx73BD<`VQT^SO#iHey_u^&*+&eH? z!+nB>gC@EaGZD&&w+d9q=aJd0`q}i+V+sCpjuebYz*D55FHD@U`eL<2ofY!|r}8lC zkn`%HIg4+*-@uSUP`+4#WW9W^GRI?+MCIzb-uDZl&91+_z_5&7j<5{(B{}c0t0LDx z`wy?0*80EfHbg$i!X#>1*}sjMjNJF!7ByKwdmjy5++VfCo|i7yRV(Yon7S%EP0#*p z&c!|3eoPIlbOAHi&&TzN`dC5yztw;#k=lg(s|IWpRRf008y8?<{eA2Jv2Ab8vY9_}X1kfR>n%`fhIvLokj#v8HvhzL99xy@Qk}cc`!7&XF};13etFiERvW?da}C>ssbc_sPB8e8>r(>1?W zXR@I6ww~cAN*VZCf@qUGR{u4pvx|)iMmB8EJH=3KnoUdbI5tI(C&EmFC-a%`_hdh~ zbA=^5P^4c8#xDOUdS*>Jov;lQ5L+D02+0YACjUQeoOw9Z-P^#Y^ysNjD4G;CNF~Xh z#!}2A$y)YMVn(*H%T6Ad6hg>Ok!3Kpu`dZ3`%{>)jloPMgJG;=81HwU=Y8Mn_s8$Q zxxV)~=f2OGbDibB&*wfiGa1<&W!)ylHO_mHcSTupPAU8J;VQ(y=Kbo%;0UH~GzO<~ zG1>EVdRhe={8CR}uco9U)xt(24R=~JtMWM!Pd?l0Vu#i7|JsNq@bzeM=!ozsxv=KukhPQcDksvCi?9Cxo4ush zXfrtp4-f;c;06V=N4k+x>jvu{?Kxmib)25LkFrBydO%)*cGX>aGW4_U>q?v!bV_)G z{s;k|u0qPsg~g^H%f-Q{d=I7@u%%7iLb25o6U6)|l6Fcnu|sI6qLHnfWZTy`1pM?Q zAmdegs3~F-xoFa;EBYR6gadtDmy}$wX_$7$X-H^l?i*R%l+cGjkbYy&-ev)}F!$6T zai~*lHJxS&+)RQF1P7h4J{3yJC)!R|X)_%XHjD&`!f$Hf;DO11`LE5emsQuvHSmQE zN3&F{r#S|XvD~<0YhG>+Y&3o6uH+YJf0ux4-t0ut4Z}*t5lf~B#Lo+iH05z05pwZ} zEr!$KVqkl1JIX*ovVaN>C;XtKvXhWmO1B!+)vkWM@Y(KRi za=q8%_QAp2Kdp9nFkhqxG;~S53}BrBJMDlW0O?VBCFiPvVeG|YlS=x~v26*d6)b+k z`ydyf%iZ#z=7C#{FFo0>S?8MhG(Uq`JLw@p9v^uOiUoSIqbKO)jB92) z!kHheTip$Bf&GiuEJ`nZym?;^*eMf>MAx?2%`7GNS+rX{XX`~!pdnTneOvAw!P2n9m>Nogv`fxN9-DKP!I)ffs(KP zM*28?$_sSKmK>@m2{k?}@FkEruzzEN$5E=%yWouo)C0wS2B31(Z3Sq3z-RKPw>eoj zOZOEMTteIyu+hMoqK41Jajtze>q`H!TaA{M*Ksp+$FZGd0ZYSY^oiB6p*Hg)7OvN61zth}%1Gfg`AjE|_fNShI4v zM{rpHSDQ;HBQOqn-I&cPpC|7VDMxVi9chpDd%g-1A|-2rJwhQLvYc{YogtZ7^yKEF zzOQ-fR$F4Ncy969+c}|hE(J&yHfK2?w5X%`wPAv7Q;7_jiG{c)pq<{_lCr{@nOH{UY>u@EJs~tNe8k^-uh$hb@!z++QMz#i4rkG zI8AXAyUck;11Ta$aR9we7aK#Vfd2!#3R#}Dl(Ffc9NE2t&>`qx*9tnj7&%+&^!Ycv z(Aa3nmw}v-uc~6i*Y@P@Xg85D!-hlZw>5qtHv`rC_EFAqASLuU0>Ry!5o6+fm3X;m zYR83{?OQvUaF?6xc6Pl^#}?dlyG;BUU7oYme{+lTuk$KXdD;-CI#dzrAreXZFNBrp zkdfr=vfxiGiI1lQ)&S_SNvri2&8&Pw6Q^Hs3jXfUY}CB8Ikxu&gPmY0fbw9g&~9mY zbI#=?f|4QtvU8H=R1@n`ICQ-)JzL+-*IQUv3yffP_u-b8V|4C zeFyH39lw-dVbJ}b5xRF*=NccBUHv#k>7#lFkE-<~9$IXTyOtZD*0rF`DW6;PJ$ z6DEOO_adZPIk^q|wr!QlRhe4{T;3}e!TL)WJ$x3LECOWx>-PPD7@rlZE@zP$p>mx5 zuvQCaK^(D12JsZ_#owF_g%d}DpI}MVyTZmv2NcChQ%j=yB}c78`G;^LTYWtEfGa@i zq5Ua^Bafk67uuz zu$vzYzhoCZ@O>!q|Ll}~!y}p>?oxINUk{ACI>>b;=9_+8gI#ee%S~CDaS(SJ@P!%N zhxtuBrcHYq0D|JZuxmL1DutyS|DmVRFJp_grzvDw1=Wr3-*2nDB{bIx-lZOcMr3jE z#BWB!*2G1y?#5nhIuB4KS`xypyDEA&?Az{rvBJ0Tx%f#ZGb+h_k;)-0ki119G#R!V zTg0Fl1-{&>$lT@KRGs=eC=GpDO8 z_TaCDt;oM;f>Iqn602?VlmQbz%Q=9W@Dt|1KX7B0-SRjI<}mcT@K^S&SWbHi+~12Q zl7vvC3_6u16)m^3R2_>AJEU|5Foh3%m-W-C34e3TJkIbZ6?Z8^LfpIP8zAxMJsyDK64r8F|%t>HKjAo-9QkX+};_=H{;lI zfXivg@7Uo%S&4y=`x}QBD`T;3FRTD}J?O9mgng7IWX=-3pAEJ2e0x=Bzm7LCA$i0Z z5Z*Y?ziNXZ5n3ir_4vq2R-Snp33I^qB@-jlIdwCRXYVD+sP(U8Iy9z6+?s(_{X|6a z5?;Oegy1M8jO30s9;j9kXD%1|wQkU0Kyc$9!SXBo2@H3CTH0Q`ZY?Vs10PX-(JBl$c@B2^|^y}hUNHW9b*=i}?*%<&FK(5kCxc#BB@kXM*wmg~0 zakGtD@g4&Lif`T=(9D0yu{mWVO^~p-m>;XZ8H7oc(L=&b_8dn%fZAgwKI)ODTux8* zgrfd&V>#}PH2OkXng2>n{lPWMy1Rsy?4-so1IcMQlVutFt05n11wY07+ixaf^8%AFTwRbq6|2$CMI|-q$WRWOlad0YLBm-UsOzf3 zjZqL#y}q3FA#36ky}JMYx`|Y_)D3>7q9h!eXvI;?l7v(t#3Q&UBpasdF-z>~Lwmc( zH9Ocp{gLTGEnxL#X115ggRu&M&yW)H8uWKK%~T^!_Or*<3w1w_Of_ybl!vzc(73AX zxG6rcZ|uiw6K>Jb2OyPN&p^`S%N^Je8zB$j}zm`xmk>$BFmWF?x4s!`%kok;%y_T~^Ci zf#PSy#Z75BpUZ-xy}F^HLo7$Wzq{APO32R*iZ#|UIw_x>-dkF+xt6bS%3e@cuQU80 zkN`l{XZ+Oy$u{iYZX`^m!r=wV+Tb@7@NzCWcu|bftK-Qb#b*BWnjv8YlF^x*du=_1 zzQt8*}bJ z;N>&FgmICAI#_pae~Dy+&NM%Fcqa7pNB+`3gVl8b3F) z$D?aEtwoupao5?!TEJSRbwMDn3xk)MrF*}uQCMe8Sc&gafA%0)V`w;)8exmV{Zrp7 zU^0D714o~dF(}lL5zDzWuuh=|mOm_+zCtR1PAx8;4G1DEFTddw{{5o9C8+a{)ytRJ zbiToXvY16%5lL2*nSUEcS|C2KSp!LojWT29@mGiG#(+RnZ#f`qr5S%u(V^vZmJ!uI z@UO+8vkl%uxWq%C^Zs!ILv3zG7f&X*s*TU9DcJMxmY(D^bmPWe3?$K*zX20MbAsmw z-TmLD=G6+(4o^cv*(Lb>I&td0^L~tU`8y!o#FT~%3_gKNK%+%SvT+Pks?l1uuL+Ivm(C%4q@#&)?#QOGE@X`Gu;kn<_0F_NGPuwJQ)y;e_Q7@kbO^ z#Ua4>SxEg_2_&&B(%YK|%a(eX9&Prko+Gg1e})VDfR?y^Ja&+xJ=iffvng}rAn0w~ zA1e=H?rsX5$@=~`aa*~uPVWRLJXj{&dMWy30_@(C>a{O#iQ5_%fZ!?vx9x`e#rZnB zLT}tP2n0sB6z!ud4S9CMip(0U|AQvm(chiW1Qkm?4+1r;!Z3SsaEWuP-*?QTo4;;p z{5}ABn-hilxEg1H&o+jcq{RneMcg$e$O)oAae-swWNFCd0Ug%Zi8ar;TiCkBN7Ft7 z4o`k2aR%ZzoQjOOtF`uRVcCK)+9D@9KEB|O^xssO@>pAaWxru3XDgf9hmkk}1o7d;ToMl**b2>tla|Y7pDPXy^8phI1CMIa4F?bDrNW2_Zjmici4vM@d+8z9eI> zSHx1Y*2OT4?AFBcM@>&-r|W+tc2PbU7cD12K%iH$flY2ek2V7L77Wd6-C~!_UQfDD zOyp!1$&>|WYKC3|W__a2@!zBmXa|hZ*!rwV-c1!bWA!tVb13bT@yEJ<)39WQZ4WT; zrP>2Qgv$_V%)^rALW@#YSs_e3{MqvWa7?%4;H!tVdO%`lhpyRqr5JoKT2|D0ZuhIg zFZa5IcyFG2ah(T1cdbso*1Qa%2-@8u+)>@0uJ7yqgL0~e3-mUSKfstD96tu}HGlU) zQ2LS9VBD*;_;g@}G3%W1U9#An){rx1TDcAdC=2ClUOR>9mR0$Lnqm+KL=Y^!b1JC! z>T6$fVwdjNrR)^IHIBpBNcqbb%PTo$g zCD1^_Jv_2X?NAx|kyVRr+s#48-J21rO#NLERf{Vl3z2=t`hi1JpOKcb!B?#{2dy?= zbJ*bJlg`!Pf%Y7!7#!*j)2S==nV6y=2=pco{V*(jgdhf|mU*w?k2;1fEROcz_K-v2 z{dSTeAkf!~(|;9H8vonDSWas>7Xpq-+40P+_y@K9}Lwab4SE#X?(t;nxq*7V)d32{AupNB2pbC`)ol5ZG73HfT z9R-E&cR);-DUihS_R$u-Z3rX!lBi)<3dZIKV}w<}4Z5`h&$nPjK@xY)n%cFV&j{-* zGt)%Nm>=&XG@9(&l044Z&r2qf` literal 0 HcmV?d00001 diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index d7081d715..da327b3f2 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -230,6 +230,7 @@ class MessageLookup extends MessageLookupByLibrary { "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Class information"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Classes"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Event details"), + "navigationFeedbackMotivation" : MessageLookupByLibrary.simpleMessage("Motivation"), "navigationFilter" : MessageLookupByLibrary.simpleMessage("Filter"), "navigationHome" : MessageLookupByLibrary.simpleMessage("Home"), "navigationMap" : MessageLookupByLibrary.simpleMessage("Map"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index ea097ccf1..fd7012518 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -230,6 +230,7 @@ class MessageLookup extends MessageLookupByLibrary { "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Informații materie"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Materii"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Detalii eveniment"), + "navigationFeedbackMotivation" : MessageLookupByLibrary.simpleMessage("Motivație"), "navigationFilter" : MessageLookupByLibrary.simpleMessage("Filtru"), "navigationHome" : MessageLookupByLibrary.simpleMessage("Acasă"), "navigationMap" : MessageLookupByLibrary.simpleMessage("Hartă"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 15dc010ea..69d6de7e5 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1735,6 +1735,16 @@ class S { ); } + /// `Motivation` + String get navigationFeedbackMotivation { + return Intl.message( + 'Motivation', + name: 'navigationFeedbackMotivation', + desc: '', + args: [], + ); + } + /// `Show all` String get filterMenuShowAll { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f3d0a93e9..e660bc4da 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -177,6 +177,7 @@ "navigationNewsFeed": "News feed", "navigationClassInfo": "Class information", "navigationClassFeedback": "Review", + "navigationFeedbackMotivation": "Motivation", "filterMenuShowAll": "Show all", "filterMenuShowMine": "Show only mine", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 11644261d..ca05314ca 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -177,6 +177,7 @@ "navigationNewsFeed": "Știri", "navigationClassInfo": "Informații materie", "navigationClassFeedback": "Feedback", + "navigationFeedbackMotivation": "Motivație", "filterMenuShowAll": "Arată tot", "filterMenuShowMine": "Arată doar pe ale mele", diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 7e5ed1326..1b6744325 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -3,6 +3,7 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_motivation.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; @@ -213,7 +214,14 @@ class _ClassFeedbackViewState extends State { child: IconText( icon: Icons.info_outline, text: S.current.infoFormAnonymous, + actionText: S.current.actionShowMore, + actionArrow: true, style: Theme.of(context).textTheme.bodyText1, + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FeedbackMotivation(), + ), + ), ), ), ), diff --git a/lib/pages/class_feedback/view/feedback_motivation.dart b/lib/pages/class_feedback/view/feedback_motivation.dart new file mode 100644 index 000000000..bf66ffdb1 --- /dev/null +++ b/lib/pages/class_feedback/view/feedback_motivation.dart @@ -0,0 +1,95 @@ +import 'package:acs_upb_mobile/widgets/scaffold.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:flutter/material.dart'; + +class FeedbackMotivation extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AppScaffold( + title: Text(S.current.navigationFeedbackMotivation), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + height: MediaQuery.of(context).size.height / 3, + child: Image.asset('assets/illustrations/undraw_review.png'), + ), + ), + const SizedBox(height: 15), + Text( + 'Share your experience so future generations of students will receive statistics about this class!', + style: Theme.of(context).textTheme.headline6, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + const Text( + 'Key aspects we take into account:', + style: TextStyle( + fontSize: 18, + ), + ), + const SizedBox(height: 15), + const Text( + '1. Your data and privacy are respected.' + ' We do not report individual responses,' + ' but these are aggregated, thus an opinion cannot' + ' be associated with a particular profile.', + style: TextStyle( + fontSize: 15, + ), + ), + const SizedBox(height: 10), + const Text( + '2. Information shared will be kept in our database for' + ' at least 4 years (study duration of a Bachelor\'s degree),' + ' so the evolution over time can be observed.', + style: TextStyle( + fontSize: 15, + ), + ), + const SizedBox(height: 10), + const Text( + '3. Access to statistics is allowed to any student' + ' who wants to find out impressions about a class' + ' during the semester. However, while the opportunity to' + ' share your review is active, only students who have' + ' already expressed their opinion can analyze the ideas' + ' of others.', + style: TextStyle( + fontSize: 15, + ), + ), + const SizedBox(height: 10), + const Text( + '4. The whole process of collecting and displaying reviews' + ' is transparent. Being an open-source application,' + ' source code is accessible to everyone.', + style: TextStyle( + fontSize: 15, + ), + ), + const SizedBox(height: 10), + const Text( + '5. We are constantly looking to improve the connection' + ' between different generations of students. As a' + ' result, any thought is extremely value. All' + ' the details supplied are used pro-actively.', + style: TextStyle( + fontSize: 15, + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 2a6c62252..9f7be0f77 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -62,7 +62,7 @@ class _ClassViewState extends State { return AppScaffold( title: Text(S.current.navigationClassInfo), actions: [ - if (widget.remoteConfigService.feedbackEnabled) + //if (widget.remoteConfigService.feedbackEnabled) AppScaffoldAction( icon: Icons.rate_review_outlined, onPressed: () { From 5e52ba3fe6bbc6fcc74d98a23491131296c89837 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 5 Jun 2021 19:48:55 +0300 Subject: [PATCH 49/59] Make dropdown answers localizable --- lib/pages/class_feedback/service/feedback_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/class_feedback/service/feedback_provider.dart b/lib/pages/class_feedback/service/feedback_provider.dart index 2f9e63f5a..20c504ced 100644 --- a/lib/pages/class_feedback/service/feedback_provider.dart +++ b/lib/pages/class_feedback/service/feedback_provider.dart @@ -29,7 +29,7 @@ extension FeedbackQuestionExtension on FeedbackQuestion { if (json['type'] == 'dropdown' && json['options'] != null) { final List options = json['options']; final List optionsString = - options.map((e) => e as String).toList(); + options.map((e) => e[LocaleProvider.localeString] as String).toList(); return FeedbackQuestionDropdown( category: json['category'], question: json['question'][LocaleProvider.localeString], From 7277b835436ef54970dedb5b61b4ddb83822e4ab Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 5 Jun 2021 19:58:25 +0300 Subject: [PATCH 50/59] Modify SizedBox height --- lib/widgets/feedback_question.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart index a993106ac..2baa22f1e 100644 --- a/lib/widgets/feedback_question.dart +++ b/lib/widgets/feedback_question.dart @@ -110,7 +110,7 @@ class _FeedbackQuestionFormState extends State { return null; }, ), - const SizedBox(height: 10), + const SizedBox(height: 16), ], ); } else if (widget.question is FeedbackQuestionText) { From 67e1b7a990feb27894ac3c79b3292cc8732d5a2b Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 6 Jun 2021 22:49:12 +0300 Subject: [PATCH 51/59] Add feedback icon tooltip --- lib/pages/classes/view/class_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 2a6c62252..fa05814fb 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -65,6 +65,7 @@ class _ClassViewState extends State { if (widget.remoteConfigService.feedbackEnabled) AppScaffoldAction( icon: Icons.rate_review_outlined, + tooltip: S.current.navigationClassFeedback, onPressed: () { if (!alreadyCompletedFeedback) { Navigator.of(context) From 5d9991d3989152dee5ee9906fed2525c7dab7b14 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 6 Jun 2021 23:13:42 +0300 Subject: [PATCH 52/59] Modify navigation message to "Learn more" --- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ro.dart | 1 + lib/generated/l10n.dart | 10 ++++++++++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ro.arb | 1 + lib/pages/class_feedback/view/class_feedback_view.dart | 2 +- lib/pages/classes/view/class_view.dart | 2 +- 7 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index da327b3f2..c6f1bf8d7 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -65,6 +65,7 @@ class MessageLookup extends MessageLookupByLibrary { "actionEditWebsite" : MessageLookupByLibrary.simpleMessage("Edit website"), "actionEnableEditing" : MessageLookupByLibrary.simpleMessage("Enable editing"), "actionJumpToToday" : MessageLookupByLibrary.simpleMessage("Jump to today"), + "actionLearnMore" : MessageLookupByLibrary.simpleMessage("Learn more"), "actionLogIn" : MessageLookupByLibrary.simpleMessage("Log in"), "actionLogInAnonymously" : MessageLookupByLibrary.simpleMessage("Log in anonymously"), "actionLogOut" : MessageLookupByLibrary.simpleMessage("Log out"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index fd7012518..03ae2cfaa 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -65,6 +65,7 @@ class MessageLookup extends MessageLookupByLibrary { "actionEditWebsite" : MessageLookupByLibrary.simpleMessage("Modifică website"), "actionEnableEditing" : MessageLookupByLibrary.simpleMessage("Activează modul editare"), "actionJumpToToday" : MessageLookupByLibrary.simpleMessage("Sari la ziua de azi"), + "actionLearnMore" : MessageLookupByLibrary.simpleMessage("Află mai multe"), "actionLogIn" : MessageLookupByLibrary.simpleMessage("Conectare"), "actionLogInAnonymously" : MessageLookupByLibrary.simpleMessage("Conectare anonimă"), "actionLogOut" : MessageLookupByLibrary.simpleMessage("Deconectare"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 69d6de7e5..b2b511666 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1055,6 +1055,16 @@ class S { ); } + /// `Learn more` + String get actionLearnMore { + return Intl.message( + 'Learn more', + name: 'actionLearnMore', + desc: '', + args: [], + ); + } + /// `Something went wrong.` String get errorSomethingWentWrong { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index e660bc4da..c3800bb00 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -106,6 +106,7 @@ "actionOpenFilter": "Open filter", "actionRequestPermissions": "Request permissions", "actionRefresh": "Refresh", + "actionLearnMore": "Learn more", "errorSomethingWentWrong": "Something went wrong.", "errorPasswordsDiffer": "The two passwords differ.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index ca05314ca..a3b49c766 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -106,6 +106,7 @@ "actionOpenFilter": "Deschide filtru", "actionRequestPermissions": "Cere permisiuni", "actionRefresh": "Reîncarcă", + "actionLearnMore": "Află mai multe", "errorSomethingWentWrong": "A apărut o problemă.", "errorPasswordsDiffer": "Cele două parole diferă.", diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 1b6744325..820a4fa67 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -214,7 +214,7 @@ class _ClassFeedbackViewState extends State { child: IconText( icon: Icons.info_outline, text: S.current.infoFormAnonymous, - actionText: S.current.actionShowMore, + actionText: S.current.actionLearnMore, actionArrow: true, style: Theme.of(context).textTheme.bodyText1, onTap: () => Navigator.of(context).push( diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 13c58a8b1..fa05814fb 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -62,7 +62,7 @@ class _ClassViewState extends State { return AppScaffold( title: Text(S.current.navigationClassInfo), actions: [ - //if (widget.remoteConfigService.feedbackEnabled) + if (widget.remoteConfigService.feedbackEnabled) AppScaffoldAction( icon: Icons.rate_review_outlined, tooltip: S.current.navigationClassFeedback, From ee8b5c46b4e52e259a7aca1e5c6f76d6231d4e33 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 6 Jun 2021 23:17:43 +0300 Subject: [PATCH 53/59] Make undraw image smaller --- lib/pages/class_feedback/view/feedback_motivation.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/class_feedback/view/feedback_motivation.dart b/lib/pages/class_feedback/view/feedback_motivation.dart index bf66ffdb1..ccaf3b0c9 100644 --- a/lib/pages/class_feedback/view/feedback_motivation.dart +++ b/lib/pages/class_feedback/view/feedback_motivation.dart @@ -17,7 +17,7 @@ class FeedbackMotivation extends StatelessWidget { children: [ Center( child: Container( - height: MediaQuery.of(context).size.height / 3, + height: MediaQuery.of(context).size.height / 3.3, child: Image.asset('assets/illustrations/undraw_review.png'), ), ), From 8bed9dbb0ec97574a8fda575d9a2cff64867d145 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Thu, 10 Jun 2021 13:07:39 +0300 Subject: [PATCH 54/59] Begin remodeling text paragraphs --- lib/generated/l10n.dart | 50 +++++++ lib/l10n/intl_en.arb | 5 + lib/l10n/intl_ro.arb | 5 + .../view/feedback_motivation.dart | 129 ++++++++++++------ 4 files changed, 149 insertions(+), 40 deletions(-) diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index b2b511666..08489b41c 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2625,6 +2625,56 @@ class S { ); } + /// `Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile.` + String get messageFeedbackMotivation1 { + return Intl.message( + 'Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile.', + name: 'messageFeedbackMotivation1', + desc: '', + args: [], + ); + } + + /// `Information shared will be kept in our database for at least 4 years (study duration of a Bachelor's degree), so the evolution over time can be observed.` + String get messageFeedbackMotivation2 { + return Intl.message( + 'Information shared will be kept in our database for at least 4 years (study duration of a Bachelor\'s degree), so the evolution over time can be observed.', + name: 'messageFeedbackMotivation2', + desc: '', + args: [], + ); + } + + /// `Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.` + String get messageFeedbackMotivation3 { + return Intl.message( + 'Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.', + name: 'messageFeedbackMotivation3', + desc: '', + args: [], + ); + } + + /// `The whole process of collecting and displaying reviews is transparent. Being an open-source application, source code is accessible to everyone.` + String get messageFeedbackMotivation4 { + return Intl.message( + 'The whole process of collecting and displaying reviews is transparent. Being an open-source application, source code is accessible to everyone.', + name: 'messageFeedbackMotivation4', + desc: '', + args: [], + ); + } + + /// `We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively.` + String get messageFeedbackMotivation5 { + return Intl.message( + 'We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively.', + name: 'messageFeedbackMotivation5', + desc: '', + args: [], + ); + } + /// `Please check your inbox for the password reset e-mail.` String get infoPasswordResetEmailSent { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c3800bb00..5614805dd 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -273,6 +273,11 @@ "messageYouCanContribute": "You can contribute to the app data, but you first need to request permissions.", "messageThereAreNoEventsForSelected": "There are no events for the selected ", "messagePictureUpdatedSuccess": "Profile picture updated successfully.", + "messageFeedbackMotivation1": "Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile.", + "messageFeedbackMotivation2": "Information shared will be kept in our database for at least 4 years (study duration of a Bachelor's degree), so the evolution over time can be observed.", + "messageFeedbackMotivation3": "Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.", + "messageFeedbackMotivation4": "The whole process of collecting and displaying reviews is transparent. Being an open-source application, source code is accessible to everyone.", + "messageFeedbackMotivation5": "We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively.", "infoPasswordResetEmailSent": "Please check your inbox for the password reset e-mail.", "infoRelevance": "Try to choose the most restrictive category.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index a3b49c766..43d1a68b2 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -273,6 +273,11 @@ "messageYouCanContribute": "Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni.", "messageThereAreNoEventsForSelected": "Nu există evenimente pentru selecția de ", "messagePictureUpdatedSuccess": "Poza a fost actualizată cu succes.", + "messageFeedbackMotivation1": "Datele și confidențialitatea ta sunt respectate. Nu colectăm răspunsuri individuale, ci acestea sunt agregate, astfel încât o opinie să nu poată fi asociată cu un profil anume.", + "messageFeedbackMotivation2": "Informațiile partajate vor fi păstrate în baza noastră de date timp de cel puțin 4 ani (durata studiului ciclului de licență), astfel încât să se poată observa evoluția în timp.", + "messageFeedbackMotivation3": "Accesul la statistici este permis oricărui student care dorește să afle impresii despre o disciplină în timpul semestrului. Cu toate acestea, în timp ce oportunitatea de a împărtăși feedback este activă, numai studenții care și-au exprimat deja opinia pot analiza ideile altora.", + "messageFeedbackMotivation4": "Întregul proces de colectare și afișare a recenziilor este transparent. Fiind o aplicație open-source, codul sursă este accesibil tuturor.", + "messageFeedbackMotivation5": "Căutăm în permanență să îmbunătățim legătura dintre diferite generații de studenți. Drept urmare, orice părere este extrem de valoroasă. Toate detaliile furnizate sunt utilizate pro-activ.", "infoPasswordResetEmailSent": "Please check your inbox for the password reset e-mail.", "infoRelevance": "Încercați să selectați cea mai restrictivă categorie.", diff --git a/lib/pages/class_feedback/view/feedback_motivation.dart b/lib/pages/class_feedback/view/feedback_motivation.dart index ccaf3b0c9..2be504588 100644 --- a/lib/pages/class_feedback/view/feedback_motivation.dart +++ b/lib/pages/class_feedback/view/feedback_motivation.dart @@ -18,7 +18,8 @@ class FeedbackMotivation extends StatelessWidget { Center( child: Container( height: MediaQuery.of(context).size.height / 3.3, - child: Image.asset('assets/illustrations/undraw_review.png'), + child: + Image.asset('assets/illustrations/undraw_review.png'), ), ), const SizedBox(height: 15), @@ -35,53 +36,101 @@ class FeedbackMotivation extends StatelessWidget { ), ), const SizedBox(height: 15), - const Text( - '1. Your data and privacy are respected.' - ' We do not report individual responses,' - ' but these are aggregated, thus an opinion cannot' - ' be associated with a particular profile.', - style: TextStyle( - fontSize: 15, + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 15, + fontFamily: 'Montserrat', + ), + children: [ + const TextSpan( + text: '1. ', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan(text: S.current.messageFeedbackMotivation1), + ], ), ), - const SizedBox(height: 10), - const Text( - '2. Information shared will be kept in our database for' - ' at least 4 years (study duration of a Bachelor\'s degree),' - ' so the evolution over time can be observed.', - style: TextStyle( - fontSize: 15, + const SizedBox(height: 40), + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 15, + fontFamily: 'Montserrat', + ), + children: [ + const TextSpan( + text: '2. ', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: S.current.messageFeedbackMotivation2, + ), + ], ), ), - const SizedBox(height: 10), - const Text( - '3. Access to statistics is allowed to any student' - ' who wants to find out impressions about a class' - ' during the semester. However, while the opportunity to' - ' share your review is active, only students who have' - ' already expressed their opinion can analyze the ideas' - ' of others.', - style: TextStyle( - fontSize: 15, + const SizedBox(height: 40), + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 15, + fontFamily: 'Montserrat', + ), + children: [ + const TextSpan( + text: '3. ', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: S.current.messageFeedbackMotivation3, + ), + ], ), ), - const SizedBox(height: 10), - const Text( - '4. The whole process of collecting and displaying reviews' - ' is transparent. Being an open-source application,' - ' source code is accessible to everyone.', - style: TextStyle( - fontSize: 15, + const SizedBox(height: 40), + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 15, + fontFamily: 'Montserrat', + ), + children: [ + const TextSpan( + text: '4. ', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: S.current.messageFeedbackMotivation4, + ), + ], ), ), - const SizedBox(height: 10), - const Text( - '5. We are constantly looking to improve the connection' - ' between different generations of students. As a' - ' result, any thought is extremely value. All' - ' the details supplied are used pro-actively.', - style: TextStyle( - fontSize: 15, + const SizedBox(height: 40), + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 15, + fontFamily: 'Montserrat', + ), + children: [ + const TextSpan( + text: '5. ', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: S.current.messageFeedbackMotivation5, + ), + ], ), ), const SizedBox(height: 10), From ef1e26ef6bc7a9fd3dc159824f60f87b8d7a8db5 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Mon, 5 Jul 2021 09:48:48 +0300 Subject: [PATCH 55/59] Add icon for each message --- lib/generated/intl/messages_en.dart | 7 ++ lib/generated/intl/messages_ro.dart | 7 ++ lib/generated/l10n.dart | 20 +++++ lib/l10n/intl_en.arb | 2 + lib/l10n/intl_ro.arb | 2 + .../view/feedback_motivation.dart | 81 ++++++++++--------- pubspec.lock | 2 +- 7 files changed, 82 insertions(+), 39 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index c6f1bf8d7..1b5ef15a2 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -204,7 +204,14 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Event added successfully."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Event deleted successfully."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Event modified successfully."), + "messageFeedbackAspects" : MessageLookupByLibrary.simpleMessage("Key aspects we take into account:"), "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("The review has been sent successfully."), + "messageFeedbackMotivation1" : MessageLookupByLibrary.simpleMessage("Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile."), + "messageFeedbackMotivation2" : MessageLookupByLibrary.simpleMessage("Information shared will be kept in our database for at least 4 years (study duration of a Bachelor\'s degree), so the evolution over time can be observed."), + "messageFeedbackMotivation3" : MessageLookupByLibrary.simpleMessage("Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others."), + "messageFeedbackMotivation4" : MessageLookupByLibrary.simpleMessage("The whole process of collecting and displaying reviews is transparent. Being an open-source application, source code is accessible to everyone."), + "messageFeedbackMotivation5" : MessageLookupByLibrary.simpleMessage("We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively."), + "messageFeedbackMotivationOverview" : MessageLookupByLibrary.simpleMessage("Share your experience so future generations of students will receive statistics about this class!"), "messageGetStartedByPressing" : MessageLookupByLibrary.simpleMessage("Get started by pressing the"), "messageIAgreeToThe" : MessageLookupByLibrary.simpleMessage("I agree to the "), "messageNewUser" : MessageLookupByLibrary.simpleMessage("New user?"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 03ae2cfaa..cdce0f119 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -204,7 +204,14 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Eveniment adăugat cu succes."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Eveniment șters cu succes."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Eveniment modificat cu succes."), + "messageFeedbackAspects" : MessageLookupByLibrary.simpleMessage("Aspecte cheie pe care le luăm în considerare:"), "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("Feedback trimis cu succes."), + "messageFeedbackMotivation1" : MessageLookupByLibrary.simpleMessage("Datele și confidențialitatea ta sunt respectate. Nu colectăm răspunsuri individuale, ci acestea sunt agregate, astfel încât o opinie să nu poată fi asociată cu un profil anume."), + "messageFeedbackMotivation2" : MessageLookupByLibrary.simpleMessage("Informațiile partajate vor fi păstrate în baza noastră de date timp de cel puțin 4 ani (durata studiului ciclului de licență), astfel încât să se poată observa evoluția în timp."), + "messageFeedbackMotivation3" : MessageLookupByLibrary.simpleMessage("Accesul la statistici este permis oricărui student care dorește să afle impresii despre o disciplină în timpul semestrului. Cu toate acestea, în timp ce oportunitatea de a împărtăși feedback este activă, numai studenții care și-au exprimat deja opinia pot analiza ideile altora."), + "messageFeedbackMotivation4" : MessageLookupByLibrary.simpleMessage("Întregul proces de colectare și afișare a recenziilor este transparent. Fiind o aplicație open-source, codul sursă este accesibil tuturor."), + "messageFeedbackMotivation5" : MessageLookupByLibrary.simpleMessage("Căutăm în permanență să îmbunătățim legătura dintre diferite generații de studenți. Drept urmare, orice părere este extrem de valoroasă. Toate detaliile furnizate sunt utilizate pro-activ."), + "messageFeedbackMotivationOverview" : MessageLookupByLibrary.simpleMessage("Împărtășiți-vă experiența, astfel încât generațiile viitoare de studenți să primească statistici despre această materie!"), "messageGetStartedByPressing" : MessageLookupByLibrary.simpleMessage("Începeți prin a apăsa butonul"), "messageIAgreeToThe" : MessageLookupByLibrary.simpleMessage("Sunt de acord cu "), "messageNewUser" : MessageLookupByLibrary.simpleMessage("Utilizator nou?"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 08489b41c..97c5da2c0 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2625,6 +2625,26 @@ class S { ); } + /// `Share your experience so future generations of students will receive statistics about this class!` + String get messageFeedbackMotivationOverview { + return Intl.message( + 'Share your experience so future generations of students will receive statistics about this class!', + name: 'messageFeedbackMotivationOverview', + desc: '', + args: [], + ); + } + + /// `Key aspects we take into account:` + String get messageFeedbackAspects { + return Intl.message( + 'Key aspects we take into account:', + name: 'messageFeedbackAspects', + desc: '', + args: [], + ); + } + /// `Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile.` String get messageFeedbackMotivation1 { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5614805dd..44e1fcd55 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -273,6 +273,8 @@ "messageYouCanContribute": "You can contribute to the app data, but you first need to request permissions.", "messageThereAreNoEventsForSelected": "There are no events for the selected ", "messagePictureUpdatedSuccess": "Profile picture updated successfully.", + "messageFeedbackMotivationOverview": "Share your experience so future generations of students will receive statistics about this class!", + "messageFeedbackAspects": "Key aspects we take into account:", "messageFeedbackMotivation1": "Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile.", "messageFeedbackMotivation2": "Information shared will be kept in our database for at least 4 years (study duration of a Bachelor's degree), so the evolution over time can be observed.", "messageFeedbackMotivation3": "Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 43d1a68b2..7b27ae343 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -273,6 +273,8 @@ "messageYouCanContribute": "Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni.", "messageThereAreNoEventsForSelected": "Nu există evenimente pentru selecția de ", "messagePictureUpdatedSuccess": "Poza a fost actualizată cu succes.", + "messageFeedbackMotivationOverview": "Împărtășiți-vă experiența, astfel încât generațiile viitoare de studenți să primească statistici despre această materie!", + "messageFeedbackAspects": "Aspecte cheie pe care le luăm în considerare:", "messageFeedbackMotivation1": "Datele și confidențialitatea ta sunt respectate. Nu colectăm răspunsuri individuale, ci acestea sunt agregate, astfel încât o opinie să nu poată fi asociată cu un profil anume.", "messageFeedbackMotivation2": "Informațiile partajate vor fi păstrate în baza noastră de date timp de cel puțin 4 ani (durata studiului ciclului de licență), astfel încât să se poată observa evoluția în timp.", "messageFeedbackMotivation3": "Accesul la statistici este permis oricărui student care dorește să afle impresii despre o disciplină în timpul semestrului. Cu toate acestea, în timp ce oportunitatea de a împărtăși feedback este activă, numai studenții care și-au exprimat deja opinia pot analiza ideile altora.", diff --git a/lib/pages/class_feedback/view/feedback_motivation.dart b/lib/pages/class_feedback/view/feedback_motivation.dart index 2be504588..96b02cc31 100644 --- a/lib/pages/class_feedback/view/feedback_motivation.dart +++ b/lib/pages/class_feedback/view/feedback_motivation.dart @@ -24,18 +24,25 @@ class FeedbackMotivation extends StatelessWidget { ), const SizedBox(height: 15), Text( - 'Share your experience so future generations of students will receive statistics about this class!', + S.current.messageFeedbackMotivationOverview, style: Theme.of(context).textTheme.headline6, textAlign: TextAlign.center, ), const SizedBox(height: 24), - const Text( - 'Key aspects we take into account:', - style: TextStyle( + Text( + S.current.messageFeedbackAspects, + style: const TextStyle( fontSize: 18, ), ), const SizedBox(height: 15), + const Center( + child: Icon( + Icons.data_usage_outlined, + size: 40, + ), + ), + const SizedBox(height: 10), RichText( text: TextSpan( style: const TextStyle( @@ -43,17 +50,18 @@ class FeedbackMotivation extends StatelessWidget { fontFamily: 'Montserrat', ), children: [ - const TextSpan( - text: '1. ', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), TextSpan(text: S.current.messageFeedbackMotivation1), ], ), ), - const SizedBox(height: 40), + const SizedBox(height: 30), + const Center( + child: Icon( + Icons.timeline_outlined, + size: 40, + ), + ), + const SizedBox(height: 10), RichText( text: TextSpan( style: const TextStyle( @@ -61,19 +69,20 @@ class FeedbackMotivation extends StatelessWidget { fontFamily: 'Montserrat', ), children: [ - const TextSpan( - text: '2. ', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), TextSpan( text: S.current.messageFeedbackMotivation2, ), ], ), ), - const SizedBox(height: 40), + const SizedBox(height: 30), + const Center( + child: Icon( + Icons.bar_chart_outlined, + size: 40, + ), + ), + const SizedBox(height: 10), RichText( text: TextSpan( style: const TextStyle( @@ -81,19 +90,20 @@ class FeedbackMotivation extends StatelessWidget { fontFamily: 'Montserrat', ), children: [ - const TextSpan( - text: '3. ', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), TextSpan( text: S.current.messageFeedbackMotivation3, ), ], ), ), - const SizedBox(height: 40), + const SizedBox(height: 30), + const Center( + child: Icon( + Icons.accessibility_new_outlined, + size: 40, + ), + ), + const SizedBox(height: 10), RichText( text: TextSpan( style: const TextStyle( @@ -101,19 +111,20 @@ class FeedbackMotivation extends StatelessWidget { fontFamily: 'Montserrat', ), children: [ - const TextSpan( - text: '4. ', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), TextSpan( text: S.current.messageFeedbackMotivation4, ), ], ), ), - const SizedBox(height: 40), + const SizedBox(height: 30), + const Center( + child: Icon( + Icons.emoji_objects_outlined, + size: 40, + ), + ), + const SizedBox(height: 10), RichText( text: TextSpan( style: const TextStyle( @@ -121,12 +132,6 @@ class FeedbackMotivation extends StatelessWidget { fontFamily: 'Montserrat', ), children: [ - const TextSpan( - text: '5. ', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), TextSpan( text: S.current.messageFeedbackMotivation5, ), diff --git a/pubspec.lock b/pubspec.lock index 7e6b01e89..713b2771e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -748,7 +748,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.11.1" petitparser: dependency: transitive description: From ecf98e5ae77c5f663488d84171f6f1a05e428bbe Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sat, 9 Oct 2021 21:07:59 +0300 Subject: [PATCH 56/59] Merge branch master into current --- lib/generated/intl/messages_en.dart | 7 +- lib/generated/intl/messages_ro.dart | 2 +- lib/generated/l10n.dart | 8 +- lib/l10n/intl_en.arb | 4 +- lib/main.dart | 4 - .../view/class_feedback_view.dart | 11 ++- lib/pages/people/view/people_page.dart | 89 ------------------- test/integration_test.dart | 8 -- 8 files changed, 17 insertions(+), 116 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 319d5eebb..0679540f3 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -230,15 +230,14 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Event added successfully."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Event deleted successfully."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Event modified successfully."), - "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("The review has been sent successfully."), - "messageFeedbackLeft" : m9, "messageFeedbackAspects" : MessageLookupByLibrary.simpleMessage("Key aspects we take into account:"), "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("The review has been sent successfully."), + "messageFeedbackLeft" : m9, "messageFeedbackMotivation1" : MessageLookupByLibrary.simpleMessage("Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile."), "messageFeedbackMotivation2" : MessageLookupByLibrary.simpleMessage("Information shared will be kept in our database for at least 4 years (study duration of a Bachelor\'s degree), so the evolution over time can be observed."), - "messageFeedbackMotivation3" : MessageLookupByLibrary.simpleMessage("Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others."), + "messageFeedbackMotivation3" : MessageLookupByLibrary.simpleMessage("Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your feedback is active, only students who have already expressed their opinion can analyze the ideas of others."), "messageFeedbackMotivation4" : MessageLookupByLibrary.simpleMessage("The whole process of collecting and displaying reviews is transparent. Being an open-source application, source code is accessible to everyone."), - "messageFeedbackMotivation5" : MessageLookupByLibrary.simpleMessage("We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively."), + "messageFeedbackMotivation5" : MessageLookupByLibrary.simpleMessage("We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely valuable. All the details supplied are used pro-actively."), "messageFeedbackMotivationOverview" : MessageLookupByLibrary.simpleMessage("Share your experience so future generations of students will receive statistics about this class!"), "messageGetStartedByPressing" : MessageLookupByLibrary.simpleMessage("Get started by pressing the"), "messageIAgreeToThe" : MessageLookupByLibrary.simpleMessage("I agree to the "), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index 5e9036825..1a02768e8 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -230,9 +230,9 @@ class MessageLookup extends MessageLookupByLibrary { "messageEventAdded" : MessageLookupByLibrary.simpleMessage("Eveniment adăugat cu succes."), "messageEventDeleted" : MessageLookupByLibrary.simpleMessage("Eveniment șters cu succes."), "messageEventEdited" : MessageLookupByLibrary.simpleMessage("Eveniment modificat cu succes."), + "messageFeedbackAspects" : MessageLookupByLibrary.simpleMessage("Aspecte cheie pe care le luăm în considerare:"), "messageFeedbackHasBeenSent" : MessageLookupByLibrary.simpleMessage("Feedback trimis cu succes."), "messageFeedbackLeft" : m9, - "messageFeedbackAspects" : MessageLookupByLibrary.simpleMessage("Aspecte cheie pe care le luăm în considerare:"), "messageFeedbackMotivation1" : MessageLookupByLibrary.simpleMessage("Datele și confidențialitatea ta sunt respectate. Nu colectăm răspunsuri individuale, ci acestea sunt agregate, astfel încât o opinie să nu poată fi asociată cu un profil anume."), "messageFeedbackMotivation2" : MessageLookupByLibrary.simpleMessage("Informațiile partajate vor fi păstrate în baza noastră de date timp de cel puțin 4 ani (durata studiului ciclului de licență), astfel încât să se poată observa evoluția în timp."), "messageFeedbackMotivation3" : MessageLookupByLibrary.simpleMessage("Accesul la statistici este permis oricărui student care dorește să afle impresii despre o disciplină în timpul semestrului. Cu toate acestea, în timp ce oportunitatea de a împărtăși feedback este activă, numai studenții care și-au exprimat deja opinia pot analiza ideile altora."), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 6ef8ad4c9..e98c70c40 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3005,10 +3005,10 @@ class S { ); } - /// `Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.` + /// `Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your feedback is active, only students who have already expressed their opinion can analyze the ideas of others.` String get messageFeedbackMotivation3 { return Intl.message( - 'Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.', + 'Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your feedback is active, only students who have already expressed their opinion can analyze the ideas of others.', name: 'messageFeedbackMotivation3', desc: '', args: [], @@ -3025,10 +3025,10 @@ class S { ); } - /// `We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively.` + /// `We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely valuable. All the details supplied are used pro-actively.` String get messageFeedbackMotivation5 { return Intl.message( - 'We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively.', + 'We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely valuable. All the details supplied are used pro-actively.', name: 'messageFeedbackMotivation5', desc: '', args: [], diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9f70cacda..9a90fc313 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -311,9 +311,9 @@ "messageFeedbackAspects": "Key aspects we take into account:", "messageFeedbackMotivation1": "Your data and privacy are respected. We do not report individual responses, but these are aggregated, thus an opinion cannot be associated with a particular profile.", "messageFeedbackMotivation2": "Information shared will be kept in our database for at least 4 years (study duration of a Bachelor's degree), so the evolution over time can be observed.", - "messageFeedbackMotivation3": "Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your review is active, only students who have already expressed their opinion can analyze the ideas of others.", + "messageFeedbackMotivation3": "Access to statistics is allowed to any student who wants to find out impressions about a class during the semester. However, while the opportunity to share your feedback is active, only students who have already expressed their opinion can analyze the ideas of others.", "messageFeedbackMotivation4": "The whole process of collecting and displaying reviews is transparent. Being an open-source application, source code is accessible to everyone.", - "messageFeedbackMotivation5": "We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely value. All the details supplied are used pro-actively.", + "messageFeedbackMotivation5": "We are constantly looking to improve the connection between different generations of students. As a result, any thought is extremely valuable. All the details supplied are used pro-actively.", "infoPasswordResetEmailSent": "Please check your inbox for the password reset e-mail.", "infoRelevance": "Try to choose the most restrictive category.", diff --git a/lib/main.dart b/lib/main.dart index 325f95a87..37467ee97 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,6 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/navigation/bottom_navigation_bar.dart'; import 'package:acs_upb_mobile/navigation/routes.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; @@ -70,8 +68,6 @@ Future main() async { Utils.packageInfo = await PackageInfo.fromPlatform(); await Firebase.initializeApp(); - final remoteConfigService = await RemoteConfigService.getInstance(); - await remoteConfigService.initialise(); final authProvider = AuthProvider(); final classProvider = ClassProvider(); diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index 4e836dea5..7752be5a5 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -3,16 +3,12 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_question.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/class_feedback_answer.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_motivation.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/widgets/feedback_question.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; @@ -218,7 +214,14 @@ class _ClassFeedbackViewState extends State { child: IconText( icon: Icons.info_outline, text: S.current.infoFormAnonymous, + actionText: S.current.actionLearnMore, + actionArrow: true, style: Theme.of(context).textTheme.bodyText1, + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FeedbackMotivation(), + ), + ), ), ), ), diff --git a/lib/pages/people/view/people_page.dart b/lib/pages/people/view/people_page.dart index 52e7af83a..dde87cbae 100644 --- a/lib/pages/people/view/people_page.dart +++ b/lib/pages/people/view/people_page.dart @@ -230,92 +230,3 @@ class _AutocompletePersonState extends State { ); } } - -class AutocompletePerson extends StatefulWidget { - const AutocompletePerson( - {@required this.labelText, - @required this.classTeachers, - Key key, - this.warning, - this.formKey, - this.onSaved, - this.personDisplayed}) - : super(key: key); - - final String labelText; - final String warning; - final GlobalKey formKey; - final List classTeachers; - final Person Function(Person) onSaved; - final Person personDisplayed; - - @override - _AutocompletePersonState createState() => _AutocompletePersonState(); -} - -class _AutocompletePersonState extends State { - Person selectedPerson; - - @override - Widget build(BuildContext context) { - return Autocomplete( - key: widget.key, - fieldViewBuilder: (BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted) { - textEditingController.text = selectedPerson?.name; - if (selectedPerson == null) { - textEditingController.text = widget.personDisplayed?.name; - } - return TextFormField( - controller: textEditingController, - decoration: InputDecoration( - labelText: widget.labelText, - prefixIcon: const Icon(FeatherIcons.user), - ), - focusNode: focusNode, - onFieldSubmitted: (String value) { - onFieldSubmitted(); - }, - validator: (_) { - if (textEditingController.text.isEmpty ?? true) { - return widget.warning; - } - return null; - }, - ); - }, - displayStringForOption: (Person person) => person.name, - optionsBuilder: (TextEditingValue textEditingValue) { - if (textEditingValue.text == '' || textEditingValue.text.isEmpty) { - return const Iterable.empty(); - } - if (widget.classTeachers.where((Person person) { - return person.name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }).isEmpty) { - final List inputTeachers = []; - final Person inputTeacher = - Person(name: textEditingValue.text.titleCase); - inputTeachers.add(inputTeacher); - return inputTeachers; - } - - return widget.classTeachers.where((Person person) { - return person.name - .toLowerCase() - .contains(textEditingValue.text.toLowerCase()); - }); - }, - onSelected: (Person selection) { - widget.formKey.currentState.validate(); - setState(() { - selectedPerson = selection; - widget.onSaved(selectedPerson); - }); - }, - ); - } -} diff --git a/test/integration_test.dart b/test/integration_test.dart index 138dea761..91f6b33af 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -3,13 +3,6 @@ import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; import 'package:acs_upb_mobile/main.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; @@ -58,7 +51,6 @@ import 'package:acs_upb_mobile/pages/timetable/view/timetable_page.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:acs_upb_mobile/resources/remote_config.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; -import 'package:acs_upb_mobile/widgets/feedback_question.dart'; import 'package:acs_upb_mobile/widgets/search_bar.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; From b93271a85100700254f1b5340bf1166b57119f6d Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 10 Oct 2021 17:56:45 +0300 Subject: [PATCH 57/59] Revert wrong merge changes --- .../class_feedback/service/remote_config.dart | 38 ----- lib/pages/classes/service/class_provider.dart | 1 - lib/pages/classes/view/class_view.dart | 4 +- lib/widgets/feedback_question.dart | 149 ------------------ lib/widgets/selectable.dart | 18 +-- 5 files changed, 10 insertions(+), 200 deletions(-) delete mode 100644 lib/pages/class_feedback/service/remote_config.dart delete mode 100644 lib/widgets/feedback_question.dart diff --git a/lib/pages/class_feedback/service/remote_config.dart b/lib/pages/class_feedback/service/remote_config.dart deleted file mode 100644 index 032af0fd1..000000000 --- a/lib/pages/class_feedback/service/remote_config.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:firebase_remote_config/firebase_remote_config.dart'; - -const String _feedbackEnabled = 'feedback_enabled'; - -class RemoteConfigService { - RemoteConfigService({RemoteConfig remoteConfig}) - : _remoteConfig = remoteConfig; - final RemoteConfig _remoteConfig; - final defaults = {_feedbackEnabled: false}; - static RemoteConfigService _instance; - - static Future getInstance() async { - return _instance ??= RemoteConfigService( - remoteConfig: await RemoteConfig.instance, - ); - } - - bool get feedbackEnabled => - // ignore: avoid_bool_literals_in_conditional_expressions - _remoteConfig == null ? true : _remoteConfig.getBool(_feedbackEnabled); - - Future initialise() async { - try { - await _remoteConfig.setDefaults(defaults); - await _fetchAndActivate(); - } on FetchThrottledException catch (e) { - print('Remote config fetch throttled: $e'); - } catch (e) { - print( - 'Unable to fetch remote config. Cached or default values will be used.'); - } - } - - Future _fetchAndActivate() async { - await _remoteConfig.fetch(); - await _remoteConfig.activateFetched(); - } -} diff --git a/lib/pages/classes/service/class_provider.dart b/lib/pages/classes/service/class_provider.dart index a2c731e12..a8aaa194f 100644 --- a/lib/pages/classes/service/class_provider.dart +++ b/lib/pages/classes/service/class_provider.dart @@ -1,6 +1,5 @@ import 'package:acs_upb_mobile/authentication/model/user.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/remote_config.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 2bffc98de..ec9d4006d 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -25,11 +25,9 @@ import 'package:provider/provider.dart'; import 'package:acs_upb_mobile/resources/remote_config.dart'; class ClassView extends StatefulWidget { - const ClassView({Key key, this.classHeader, this.remoteConfigService}) - : super(key: key); + const ClassView({Key key, this.classHeader}) : super(key: key); final ClassHeader classHeader; - final RemoteConfigService remoteConfigService; @override _ClassViewState createState() => _ClassViewState(); diff --git a/lib/widgets/feedback_question.dart b/lib/widgets/feedback_question.dart deleted file mode 100644 index 2baa22f1e..000000000 --- a/lib/widgets/feedback_question.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; -import 'package:acs_upb_mobile/widgets/radio_emoji.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:acs_upb_mobile/generated/l10n.dart'; - -class FeedbackQuestionForm extends StatefulWidget { - const FeedbackQuestionForm({ - this.question, - this.answerValues, - this.formKey, - }); - - final FeedbackQuestion question; - final List> answerValues; - final GlobalKey formKey; - - @override - _FeedbackQuestionFormState createState() => _FeedbackQuestionFormState(); -} - -class _FeedbackQuestionFormState extends State { - @override - Widget build(BuildContext context) { - if (widget.question is FeedbackQuestionSlider) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.question.question, - style: const TextStyle( - fontSize: 18, - ), - ), - Slider.adaptive( - key: const Key('FeedbackSlider'), - value: widget.question.answer != null - ? double.parse(widget.question.answer) - : 5, - onChanged: (newRating) { - setState(() { - widget.question.answer = newRating.toString(); - }); - }, - min: 1, - max: 10, - divisions: 9, - label: widget.question.answer, - activeColor: Theme.of(context).accentColor, - ), - const SizedBox(height: 10), - ], - ); - } else if (widget.question is FeedbackQuestionRating) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - EmojiFormField( - question: widget.question.question, - onSaved: (value) { - widget.question.answer = value.keys - .firstWhere((element) => value[element] == true, - orElse: () => -1) - .toString(); - }, - answerValues: widget.answerValues[int.parse(widget.question.id)], - ), - const SizedBox(height: 10), - ], - ); - } else if (widget.question is FeedbackQuestionDropdown) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.question.question, - style: const TextStyle( - fontSize: 18, - ), - ), - DropdownButtonFormField( - key: const Key('FeedbackDropdown'), - decoration: InputDecoration( - labelText: S.current.labelAnswer, - prefixIcon: const Icon(Icons.list_outlined), - ), - onSaved: (value) { - widget.question.answer = value; - }, - items: (widget.question as FeedbackQuestionDropdown) - .options - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type.toString()), - ), - ) - .toList(), - onChanged: (selection) { - widget.formKey.currentState.validate(); - }, - validator: (selection) { - if (selection == null) { - return S.current.errorAnswerCannotBeEmpty; - } - return null; - }, - ), - const SizedBox(height: 16), - ], - ); - } else if (widget.question is FeedbackQuestionText) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.question.question, - style: const TextStyle( - fontSize: 18, - ), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - TextFormField( - key: const Key('FeedbackText'), - onSaved: (value) { - widget.question.answer = value; - }, - keyboardType: TextInputType.multiline, - maxLines: null, - ), - ], - ), - ), - const SizedBox(height: 24), - ], - ); - } else { - return Container(); - } - } -} diff --git a/lib/widgets/selectable.dart b/lib/widgets/selectable.dart index f52f78446..f8462d5d7 100644 --- a/lib/widgets/selectable.dart +++ b/lib/widgets/selectable.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; class SelectableController { - SelectableState selectableState; + _SelectableState _selectableState; - bool get isSelected => selectableState?.isSelected; + bool get isSelected => _selectableState?._isSelected; void select() { - if (selectableState == null) return; - if (!isSelected) selectableState.isSelected = true; + if (_selectableState == null) return; + if (!isSelected) _selectableState.isSelected = true; } void deselect() { - if (selectableState == null) return; - if (isSelected) selectableState.isSelected = false; + if (_selectableState == null) return; + if (isSelected) _selectableState.isSelected = false; } } @@ -31,10 +31,10 @@ class Selectable extends StatefulWidget { final bool disabled; @override - SelectableState createState() => SelectableState(); + _SelectableState createState() => _SelectableState(); } -class SelectableState extends State { +class _SelectableState extends State { bool _isSelected; set isSelected(bool newValue) { @@ -52,7 +52,7 @@ class SelectableState extends State { @override Widget build(BuildContext context) { - widget.controller?.selectableState = this; + widget.controller?._selectableState = this; return Container( decoration: BoxDecoration( From 229adc594168817f53f190706bad9e51d0bca428 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 10 Oct 2021 18:04:16 +0300 Subject: [PATCH 58/59] Bump build number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5f6bffeb4..a7b6a7856 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.3.1+39 +version: 1.3.1+40 environment: sdk: ">=2.7.0 <3.0.0" From 6d89af3d944670d37e61026e4a3a658eceb8ba22 Mon Sep 17 00:00:00 2001 From: Andrei-Constantin Mirciu Date: Sun, 10 Oct 2021 18:36:05 +0300 Subject: [PATCH 59/59] Add tests for feedback motivation page --- test/integration_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/integration_test.dart b/test/integration_test.dart index 91f6b33af..194ef1e96 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -8,6 +8,7 @@ import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_sli import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; +import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_motivation.dart'; import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_question.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; @@ -1508,6 +1509,19 @@ Future main() async { expect(find.byType(ClassFeedbackView), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_forward_ios_outlined)); + await tester.pumpAndSettle(); + await tester.drag( + find.byIcon(Icons.timeline_outlined), const Offset(0, -300)); + await tester.pumpAndSettle(); + + expect(find.byType(FeedbackMotivation), findsOneWidget); + + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + + expect(find.byType(ClassFeedbackView), findsOneWidget); + await tester.tap(find.byKey(const Key('AcknowledgementCheckbox'))); await tester.pumpAndSettle();