diff --git a/Frontend/lib/mih_components/mih_objects/arguments.dart b/Frontend/lib/mih_components/mih_objects/arguments.dart index 5d85ec92..3f1cba68 100644 --- a/Frontend/lib/mih_components/mih_objects/arguments.dart +++ b/Frontend/lib/mih_components/mih_objects/arguments.dart @@ -31,6 +31,16 @@ class BusinessArguments { ); } +class BusinessViewArguments { + final Business business; + final String? startUpSearch; + + BusinessViewArguments( + this.business, + this.startUpSearch, + ); +} + class HomeArguments { final AppUser signedInUser; final BusinessUser? businessUser; @@ -218,6 +228,16 @@ class MzansiAiArguments { ); } +class MzansiDirectoryArguments { + final String? startUpSearch; + final bool personalSearch; + + MzansiDirectoryArguments( + this.startUpSearch, + this.personalSearch, + ); +} + class TestArguments { final AppUser user; final Business? business; diff --git a/Frontend/lib/mih_components/mih_objects/business_review.dart b/Frontend/lib/mih_components/mih_objects/business_review.dart new file mode 100644 index 00000000..5aed0bd9 --- /dev/null +++ b/Frontend/lib/mih_components/mih_objects/business_review.dart @@ -0,0 +1,46 @@ +class BusinessReview { + final int idbusiness_ratings; + final String app_id; + final String business_id; + final String rating_title; + final String rating_description; + final String rating_score; + final String date_time; + final String reviewer; + + BusinessReview({ + required this.idbusiness_ratings, + required this.app_id, + required this.business_id, + required this.rating_title, + required this.rating_description, + required this.rating_score, + required this.date_time, + required this.reviewer, + }); + factory BusinessReview.fromJson(Map json) { + return switch (json) { + { + "idbusiness_ratings": int idbusiness_ratings, + "app_id": String app_id, + "business_id": String business_id, + "rating_title": String rating_title, + "rating_description": String rating_description, + "rating_score": String rating_score, + "date_time": String date_time, + "reviewer": String reviewer, + } => + BusinessReview( + idbusiness_ratings: idbusiness_ratings, + app_id: app_id, + business_id: business_id, + rating_title: rating_title, + rating_description: rating_description, + rating_score: rating_score, + date_time: date_time, + reviewer: reviewer, + ), + _ => throw const FormatException('Failed to load loyalty card objects'), + }; + } +} diff --git a/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_one.dart b/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_one.dart index 2975b567..6195fabd 100644 --- a/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_one.dart +++ b/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_one.dart @@ -9,7 +9,7 @@ import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_ import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_business_profile_preview.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_personal_profile_preview.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; -import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_business_info_card.dart'; +// import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_business_info_card.dart'; import 'package:mzansi_innovation_hub/mih_services/mih_alert_services.dart'; import 'package:mzansi_innovation_hub/mih_services/mih_location_services.dart'; import 'package:mzansi_innovation_hub/mih_services/mih_validation_services.dart'; @@ -291,14 +291,16 @@ class _PackageToolOneState extends State { } }), const SizedBox(height: 10), - MihBusinessCard( - businessName: "Mzansi Innovation Hub", - cellNumber: "0788300006", - email: "yasien.meth@mzansi-innovation-hub.co.za", - gpsLocation: "-26.1853611, 28.134664", - website: - "https://app.mzansi-innovation-hub.co.za/privacy.html", - ), + // MihBusinessCard( + // businessid: "123456", + // businessName: "Mzansi Innovation Hub", + // cellNumber: "0788300006", + // email: "yasien.meth@mzansi-innovation-hub.co.za", + // gpsLocation: "-26.1853611, 28.134664", + // website: + // "https://app.mzansi-innovation-hub.co.za/privacy.html", + // rating: 3.25, + // ), const SizedBox(height: 10), Divider( color: diff --git a/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_two.dart b/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_two.dart index 1472fb95..f56ce322 100644 --- a/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_two.dart +++ b/Frontend/lib/mih_components/mih_package_components/Example/package_tools/package_tool_two.dart @@ -1,3 +1,4 @@ +import 'package:custom_rating_bar/custom_rating_bar.dart'; import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/main.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_single_child_scroll.dart'; @@ -36,6 +37,14 @@ class _PackageToolTwoState extends State { ), ), const SizedBox(height: 10), + RatingBar( + filledIcon: Icons.star, + emptyIcon: Icons.star_border, + onRatingChanged: (value) => debugPrint('$value'), + initialRating: 3, + maxRating: 5, + ), + const SizedBox(height: 10), Container( color: Colors.black, width: 200, diff --git a/Frontend/lib/mih_config/mih_routeGenerator.dart b/Frontend/lib/mih_config/mih_routeGenerator.dart index 130a1c98..8e05353d 100644 --- a/Frontend/lib/mih_config/mih_routeGenerator.dart +++ b/Frontend/lib/mih_config/mih_routeGenerator.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_layout/mih_print_prevew.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_objects/app_user.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_objects/arguments.dart'; -import 'package:mzansi_innovation_hub/mih_components/mih_objects/business.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/Example/package_test.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_notification_message.dart'; import 'package:mzansi_innovation_hub/mih_packages/about_mih/about_mih.dart'; @@ -122,13 +121,13 @@ class RouteGenerator { // } // break; // Use break and fall through to _errorRoute if argument type is wrong case AppRoutes.mzansiDirectory: - // if (args is AuthArguments) { - return MaterialPageRoute( - settings: settings, - builder: (_) => MzansiDirectory(), - ); - // } - // break; + if (args is MzansiDirectoryArguments) { + return MaterialPageRoute( + settings: settings, + builder: (_) => MzansiDirectory(arguments: args), + ); + } + break; case AppRoutes.notifications: if (args is NotificationArguments) { return MaterialPageRoute( @@ -188,10 +187,10 @@ class RouteGenerator { break; case AppRoutes.businessProfileView: - if (args is Business) { + if (args is BusinessViewArguments) { return MaterialPageRoute( settings: settings, - builder: (_) => MzansiBusinessProfileView(business: args), + builder: (_) => MzansiBusinessProfileView(arguments: args), ); } break; diff --git a/Frontend/lib/mih_packages/mih_authentication/package_tools/mih_sign_in.dart b/Frontend/lib/mih_packages/mih_authentication/package_tools/mih_sign_in.dart index 7f75000a..7f7f9e56 100644 --- a/Frontend/lib/mih_packages/mih_authentication/package_tools/mih_sign_in.dart +++ b/Frontend/lib/mih_packages/mih_authentication/package_tools/mih_sign_in.dart @@ -394,48 +394,51 @@ class _MihSignInState extends State { //spacer const SizedBox(height: 35), - Center( - child: SizedBox( - width: width, - //height: 100.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Flexible( - flex: 1, - child: Padding( - padding: EdgeInsets.only(right: 10.0), - child: Divider(), - ), - ), - Flexible( - flex: 1, - child: GestureDetector( - child: Text( - 'Use Sandox Profile', - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - color: MzansiInnovationHub.of(context)! - .theme - .secondaryColor()), + Visibility( + visible: AppEnviroment.getEnv() == "Dev", + child: Center( + child: SizedBox( + width: width, + //height: 100.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Flexible( + flex: 1, + child: Padding( + padding: EdgeInsets.only(right: 10.0), + child: Divider(), ), - onTap: () { - setState(() { - showProfiles = !showProfiles; - }); - }, ), - ), - const Flexible( - flex: 1, - child: Padding( - padding: EdgeInsets.only(left: 10.0), - child: Divider(), + Flexible( + flex: 1, + child: GestureDetector( + child: Text( + 'Use Sandox Profile', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: MzansiInnovationHub.of(context)! + .theme + .secondaryColor()), + ), + onTap: () { + setState(() { + showProfiles = !showProfiles; + }); + }, + ), ), - ), - ], + const Flexible( + flex: 1, + child: Padding( + padding: EdgeInsets.only(left: 10.0), + child: Divider(), + ), + ), + ], + ), ), ), ), diff --git a/Frontend/lib/mih_packages/mzansi_directory/builders/build_business_search_resultsList.dart b/Frontend/lib/mih_packages/mzansi_directory/builders/build_business_search_resultsList.dart index 02b918c3..c9ac8668 100644 --- a/Frontend/lib/mih_packages/mzansi_directory/builders/build_business_search_resultsList.dart +++ b/Frontend/lib/mih_packages/mzansi_directory/builders/build_business_search_resultsList.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/arguments.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_objects/business.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_business_profile_preview.dart'; class BuildBusinessSearchResultsList extends StatefulWidget { final List businessList; final String myLocation; + final String? startUpSearch; const BuildBusinessSearchResultsList({ super.key, required this.businessList, required this.myLocation, + required this.startUpSearch, }); @override @@ -37,7 +40,10 @@ class _BuildBusinessSearchResultsListState onTap: () { Navigator.of(context).pushNamed( '/business-profile/view', - arguments: widget.businessList[index], + arguments: BusinessViewArguments( + widget.businessList[index], + widget.businessList[index].Name, + ), ); }, splashColor: MzansiInnovationHub.of(context)! diff --git a/Frontend/lib/mih_packages/mzansi_directory/mzansi_directory.dart b/Frontend/lib/mih_packages/mzansi_directory/mzansi_directory.dart index 0706314b..3ddc5e55 100644 --- a/Frontend/lib/mih_packages/mzansi_directory/mzansi_directory.dart +++ b/Frontend/lib/mih_packages/mzansi_directory/mzansi_directory.dart @@ -1,11 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/arguments.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_action.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tools.dart'; import 'package:mzansi_innovation_hub/mih_packages/mzansi_directory/package_tools/mih_search_mzansi.dart'; class MzansiDirectory extends StatefulWidget { - const MzansiDirectory({super.key}); + final MzansiDirectoryArguments arguments; + const MzansiDirectory({ + super.key, + required this.arguments, + }); @override State createState() => _MzansiDirectoryState(); @@ -32,7 +37,10 @@ class _MzansiDirectoryState extends State { List getToolBody() { List toolBodies = [ - MihSearchMzansi(), + MihSearchMzansi( + startUpSearch: widget.arguments.startUpSearch, + personalSearch: widget.arguments.personalSearch, + ), // MihContacts(), // MihFavouriteBusinesses(), ]; diff --git a/Frontend/lib/mih_packages/mzansi_directory/package_tiles/mzansi_directory_tile.dart b/Frontend/lib/mih_packages/mzansi_directory/package_tiles/mzansi_directory_tile.dart index 601ef5d9..d74b46ab 100644 --- a/Frontend/lib/mih_packages/mzansi_directory/package_tiles/mzansi_directory_tile.dart +++ b/Frontend/lib/mih_packages/mzansi_directory/package_tiles/mzansi_directory_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/arguments.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_icons.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tile.dart'; @@ -21,7 +22,10 @@ class _MzansiDirectoryTileState extends State { onTap: () { Navigator.of(context).pushNamed( '/mzansi-directory', - // arguments: WalletArguments(widget.signedInUser, 0), + arguments: MzansiDirectoryArguments( + null, // startUpSearch + true, // personalSearch + ), ); }, appName: "Mzansi Directory", diff --git a/Frontend/lib/mih_packages/mzansi_directory/package_tools/mih_search_mzansi.dart b/Frontend/lib/mih_packages/mzansi_directory/package_tools/mih_search_mzansi.dart index b36bbadf..dcb4e55f 100644 --- a/Frontend/lib/mih_packages/mzansi_directory/package_tools/mih_search_mzansi.dart +++ b/Frontend/lib/mih_packages/mzansi_directory/package_tools/mih_search_mzansi.dart @@ -15,7 +15,13 @@ import 'package:mzansi_innovation_hub/mih_services/mih_location_services.dart'; import 'package:mzansi_innovation_hub/mih_services/mih_user_services.dart'; class MihSearchMzansi extends StatefulWidget { - const MihSearchMzansi({super.key}); + final String? startUpSearch; + final bool personalSearch; + const MihSearchMzansi({ + super.key, + required this.startUpSearch, + required this.personalSearch, + }); @override State createState() => _MihSearchMzansiState(); @@ -24,7 +30,7 @@ class MihSearchMzansi extends StatefulWidget { class _MihSearchMzansiState extends State { final TextEditingController mzansiSearchController = TextEditingController(); final FocusNode searchFocusNode = FocusNode(); - bool userSearch = true; + late bool userSearch; Future?> futureUserSearchResults = Future.value(); Future?> futureBusinessSearchResults = Future.value(); late Future futurePosition = @@ -41,6 +47,17 @@ class _MihSearchMzansiState extends State { @override void initState() { super.initState(); + setState(() { + userSearch = widget.personalSearch; + mzansiSearchController.text = widget.startUpSearch ?? ""; + if (userSearch) { + futureUserSearchResults = + MihUserServices().searchUsers(mzansiSearchController.text, context); + } else { + futureBusinessSearchResults = MihBusinessDetailsServices() + .searchBusinesses(mzansiSearchController.text, context); + } + }); } @override @@ -82,7 +99,7 @@ class _MihSearchMzansiState extends State { }); }, icon: Icon( - Icons.display_settings, + Icons.swap_horiz_rounded, size: 35, color: MzansiInnovationHub.of(context)!.theme.primaryColor(), @@ -246,6 +263,7 @@ class _MihSearchMzansiState extends State { BuildBusinessSearchResultsList( businessList: snapshot.requireData!, myLocation: myLocation, + startUpSearch: mzansiSearchController.text, ), ], ); diff --git a/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_business_info_card.dart b/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_business_info_card.dart index e3d0c840..c0137b42 100644 --- a/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_business_info_card.dart +++ b/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_business_info_card.dart @@ -1,21 +1,38 @@ +import 'package:custom_rating_bar/custom_rating_bar.dart'; import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business_review.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_alert.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_review_business_window.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_mzansi_directory_services.dart'; +import 'package:supertokens_flutter/supertokens.dart'; import 'package:url_launcher/url_launcher.dart'; class MihBusinessCard extends StatefulWidget { - final String businessName; - final String cellNumber; - final String email; - final String gpsLocation; - final String? website; + final Business business; + final String? startUpSearch; + // final String businessid; + // final String businessName; + // final String cellNumber; + // final String email; + // final String gpsLocation; + // final String? website; + // final double rating; + final double width; const MihBusinessCard({ super.key, - required this.businessName, - required this.cellNumber, - required this.email, - required this.gpsLocation, - this.website, + required this.business, + required this.startUpSearch, + // required this.businessid, + // required this.businessName, + // required this.cellNumber, + // required this.email, + // required this.gpsLocation, + // required this.rating, + // this.website, + required this.width, }); @override @@ -41,7 +58,7 @@ class _MihBusinessCardState extends State { alertBody: Column( children: [ Text( - "We couldn't open your phone app to call ${widget.cellNumber}. To fix this, make sure you have a phone application installed and it's set as your default dialer.", + "We couldn't open your phone app to call ${widget.business.contact_no}. To fix this, make sure you have a phone application installed and it's set as your default dialer.", style: TextStyle( color: MzansiInnovationHub.of(context)! .theme @@ -91,7 +108,7 @@ class _MihBusinessCardState extends State { alertBody: Column( children: [ Text( - "We couldn't launch your email app to send a message to ${widget.email}. To fix this, please confirm that you have an email application installed and that it's set as your default.", + "We couldn't launch your email app to send a message to ${widget.business.bus_email}. To fix this, please confirm that you have an email application installed and that it's set as your default.", style: TextStyle( color: MzansiInnovationHub.of(context)! .theme @@ -132,7 +149,7 @@ class _MihBusinessCardState extends State { alertBody: Column( children: [ Text( - "There was an issue opening maps for ${widget.businessName}. This usually happens if you don't have a maps app installed or it's not set as your default. Please install one to proceed.", + "There was an issue opening maps for ${widget.business.Name}. This usually happens if you don't have a maps app installed or it's not set as your default. Please install one to proceed.", style: TextStyle( color: MzansiInnovationHub.of(context)! .theme @@ -161,7 +178,7 @@ class _MihBusinessCardState extends State { alertBody: Column( children: [ Text( - "There was an issue opening maps for ${widget.businessName}. This usually happens if you don't have a maps app installed or it's not set as your default. Please install one to proceed.", + "There was an issue opening maps for ${widget.business.Name}. This usually happens if you don't have a maps app installed or it's not set as your default. Please install one to proceed.", style: TextStyle( color: MzansiInnovationHub.of(context)! .theme @@ -319,8 +336,17 @@ class _MihBusinessCardState extends State { ); } + Future getUserReview() async { + String user_id = await SuperTokens.getUserId(); + return await MihMzansiDirectoryServices().getUserReviewOfBusiness( + user_id, + widget.business.business_id, + ); + } + @override Widget build(BuildContext context) { + // double screenWidth = MediaQuery.of(context).size.width; return Material( color: MzansiInnovationHub.of(context)! .theme @@ -336,6 +362,36 @@ class _MihBusinessCardState extends State { ), child: Column( children: [ + const SizedBox(height: 10), + RatingBar.readOnly( + size: 50, + alignment: Alignment.center, + filledIcon: Icons.star, + emptyIcon: Icons.star_border, + halfFilledIcon: Icons.star_half, + filledColor: const Color(0xffe9e8a1), + // MzansiInnovationHub.of(context)!.theme.primaryColor(), + emptyColor: MzansiInnovationHub.of(context)!.theme.primaryColor(), + halfFilledColor: const Color(0xffe9e8a1), + // MzansiInnovationHub.of(context)!.theme.primaryColor(), + isHalfAllowed: true, + initialRating: widget.business.rating.isNotEmpty + ? double.parse(widget.business.rating) + : 0, + maxRating: 5, + ), + // Text( + // "Rating: ${widget.rating}", + // style: TextStyle( + // fontSize: 15, + // fontWeight: FontWeight.bold, + // color: MzansiInnovationHub.of(context)!.theme.primaryColor(), + // height: 1.0, + // ), + // ), + // Divider( + // color: MzansiInnovationHub.of(context)!.theme.primaryColor(), + // ), const SizedBox(height: 10), _buildContactInfo( "Call", @@ -344,7 +400,7 @@ class _MihBusinessCardState extends State { const Color(0xffaff0b3), () { // print("Calling ${widget.cellNumber}"); - _makePhoneCall(widget.cellNumber); + _makePhoneCall(widget.business.contact_no); }, ), Divider( @@ -358,9 +414,9 @@ class _MihBusinessCardState extends State { () { // print("Emailing ${widget.email}"); _launchEmail( - widget.email, - "Inquiery about ${widget.businessName}", - "Dear ${widget.businessName},\n\nI would like to inquire about your services.\n\nBest regards,\n", + widget.business.bus_email, + "Inquiery about ${widget.business.Name}", + "Dear ${widget.business.Name},\n\nI would like to inquire about your services.\n\nBest regards,\n", ); }, ), @@ -371,11 +427,12 @@ class _MihBusinessCardState extends State { "Location", "Come visit us.", Icons.location_on, - const Color(0xffe9e8a1), + const Color(0xffd69d7d), () { - final latitude = double.parse(widget.gpsLocation.split(',')[0]); + final latitude = + double.parse(widget.business.gps_location.split(',')[0]); final longitude = - double.parse(widget.gpsLocation.split(',')[1]); + double.parse(widget.business.gps_location.split(',')[1]); _launchGoogleMapsWithUrl( latitude: latitude, longitude: longitude, @@ -383,39 +440,40 @@ class _MihBusinessCardState extends State { }, ), Visibility( - visible: widget.website != null && widget.website! != "", + visible: widget.business.website.isNotEmpty && + widget.business.website != "", child: Divider( color: MzansiInnovationHub.of(context)!.theme.primaryColor(), ), ), Visibility( - visible: widget.website != null && widget.website! != "", + visible: widget.business.website.isNotEmpty && + widget.business.website != "", child: _buildContactInfo( "Website", "Find out more about us.", Icons.vpn_lock, const Color(0xffd67d8a), () { - _launchWebsite(widget.website!); + _launchWebsite(widget.business.website); }, ), ), - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 10.0), - // child: Divider( - // color: MzansiInnovationHub.of(context)!.theme.primaryColor(), - // ), - // ), - // _buildContactInfo( - // "Rate Us", - // "Let us know how we are doing.", - // Icons.star_rate_rounded, - // const Color(0xffd69d7d), - // () { - // print("Opeining rating dialog"); - // // _launchWebsite(widget.website); - // }, - // ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Divider( + color: MzansiInnovationHub.of(context)!.theme.primaryColor(), + ), + ), + _buildContactInfo( + "Rate Us", + "Let us know how we are doing.", + Icons.star_rate_rounded, + const Color(0xffe9e8a1), + () { + businessReviewRatingWindow(true, widget.width); + }, + ), // Padding( // padding: const EdgeInsets.symmetric(horizontal: 10.0), // child: Divider( @@ -444,4 +502,52 @@ class _MihBusinessCardState extends State { ), ); } + + Future businessReviewRatingWindow( + bool previouslyRated, double width) async { + showDialog( + context: context, + builder: (context) => FutureBuilder( + future: getUserReview(), + builder: (context, asyncSnapshot) { + if (asyncSnapshot.connectionState == ConnectionState.waiting) { + return const Mihloadingcircle( + message: "Checking for previous reviews...", + ); + } else if (asyncSnapshot.connectionState == ConnectionState.done) { + return MihReviewBusinessWindow( + business: widget.business, + businessReview: asyncSnapshot.data, + screenWidth: width, + readOnly: false, + startUpSearch: widget.startUpSearch, + ); + } else { + return MihPackageAlert( + alertColour: MzansiInnovationHub.of(context)!.theme.errorColor(), + alertIcon: Icon( + Icons.warning_rounded, + size: 100, + color: MzansiInnovationHub.of(context)!.theme.errorColor(), + ), + alertTitle: "Error Pulling Data", + alertBody: Column( + children: [ + Text( + "Please ensure you are connectede top the internet and you are running the latest version of MIH then try again.", + style: TextStyle( + color: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + fontSize: 15, + ), + ), + ], + ), + ); + } + }, + ), + ); + } } diff --git a/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_review_business_window.dart b/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_review_business_window.dart new file mode 100644 index 00000000..c1f02028 --- /dev/null +++ b/Frontend/lib/mih_packages/mzansi_profile/business_profile/components/mih_review_business_window.dart @@ -0,0 +1,537 @@ +import 'package:custom_rating_bar/custom_rating_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/arguments.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business_review.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_button.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_form.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_alert.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_window.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_single_child_scroll.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_text_form_field.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_alert_services.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_mzansi_directory_services.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_validation_services.dart'; +import 'package:supertokens_flutter/supertokens.dart'; + +class MihReviewBusinessWindow extends StatefulWidget { + final Business business; + final BusinessReview? businessReview; + final double screenWidth; + final bool readOnly; + final String? startUpSearch; + const MihReviewBusinessWindow({ + super.key, + required this.business, + required this.businessReview, + required this.screenWidth, + required this.readOnly, + required this.startUpSearch, + }); + + @override + State createState() => + _MihReviewBusinessWindowState(); +} + +class _MihReviewBusinessWindowState extends State { + final _formKey = GlobalKey(); + final TextEditingController _reviewTitleController = TextEditingController(); + final TextEditingController _reviewScoreController = TextEditingController(); + final TextEditingController _reviewReviewerController = + TextEditingController(); + final TextEditingController _reviewDescriptionController = + TextEditingController(); + late final VoidCallback _reviewDescriptionListener; + final ValueNotifier _counter = ValueNotifier(0); + String userId = ""; + + void showDeleteReviewAlert() { + showDialog( + context: context, + builder: (context) => MihPackageAlert( + alertColour: MzansiInnovationHub.of(context)!.theme.errorColor(), + alertIcon: Icon( + Icons.warning_rounded, + size: 100, + color: MzansiInnovationHub.of(context)!.theme.errorColor(), + ), + alertTitle: "Delete Review", + alertBody: Column( + children: [ + Text( + "Are you sure you want to delete this review? This action cannot be undone.", + style: TextStyle( + color: MzansiInnovationHub.of(context)!.theme.secondaryColor(), + fontSize: 15, + ), + ), + const SizedBox(height: 25), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + MihButton( + width: 300, + onPressed: () async { + showDialog( + context: context, + builder: (context) { + return const Mihloadingcircle(); + }, + ); + await MihMzansiDirectoryServices() + .deleteBusinessReview( + widget.businessReview!.idbusiness_ratings, + widget.businessReview!.business_id, + widget.businessReview!.rating_score, + widget.business.rating, + ) + .then((statusCode) { + Navigator.of(context).pop(); //Remove loading dialog + Navigator.of(context).pop(); //Remove delete dialog + if (statusCode == 200) { + Navigator.of(context).pop(); //Remove window + Navigator.of(context).pop(); //Remove profile + Navigator.of(context).pop(); //Remove directory + Navigator.of(context).pushNamed( + '/mzansi-directory', + arguments: MzansiDirectoryArguments( + widget.startUpSearch, // startUpSearch + false, // personalSearch + ), + ); + MihAlertServices().successAlert( + "Successfully Deleted Review!", + "Your review has successfully been delete and will no longer appear under the business.", + context, + ); + } else { + MihAlertServices().errorAlert( + "Error Deleting Review", + "There was an error deleting your review. Please try again later.", + context, + ); + } + }); + }, + buttonColor: + MzansiInnovationHub.of(context)!.theme.errorColor(), + child: Text( + "Delete", + style: TextStyle( + color: + MzansiInnovationHub.of(context)!.theme.primaryColor(), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + MihButton( + width: 300, + onPressed: () { + Navigator.of(context).pop(); + }, + buttonColor: + MzansiInnovationHub.of(context)!.theme.successColor(), + child: Text( + "Cancel", + style: TextStyle( + color: + MzansiInnovationHub.of(context)!.theme.primaryColor(), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Color getMissionVisionLimitColor(int limit) { + if (_counter.value <= limit) { + return MzansiInnovationHub.of(context)!.theme.secondaryColor(); + } else { + return MzansiInnovationHub.of(context)!.theme.errorColor(); + } + } + + void submitForm() async { + showDialog( + context: context, + builder: (context) { + return const Mihloadingcircle(); + }, + ); + if (widget.businessReview != null) { + await MihMzansiDirectoryServices() + .updateBusinessReview( + widget.businessReview!.idbusiness_ratings, + widget.businessReview!.business_id, + _reviewTitleController.text, + _reviewDescriptionController.text, + _reviewScoreController.text, + widget.businessReview!.rating_score, + widget.business.rating, + ) + .then((statusCode) { + Navigator.of(context).pop(); //Remove loading dialog + if (statusCode == 200) { + Navigator.of(context).pop(); //pop window + Navigator.of(context).pop(); //pop business profile + Navigator.of(context).pop(); //pop directory + Navigator.of(context).pushNamed( + '/mzansi-directory', + arguments: MzansiDirectoryArguments( + widget.startUpSearch, // startUpSearch + false, // personalSearch + ), + ); + MihAlertServices().successAlert( + "Successfully Updated Review!", + "Your review has successfully been updated and will now appear under the business.", + context, + ); + } else { + MihAlertServices().errorAlert( + "Error Updating Review", + "There was an error updating your review. Please try again later.", + context, + ); + } + }); + } else { + await MihMzansiDirectoryServices() + .addBusinessReview( + userId, + widget.business.business_id, + _reviewTitleController.text, + _reviewDescriptionController.text, + _reviewScoreController.text, + widget.business.rating.isEmpty ? "0.0" : widget.business.rating, + ) + .then((statusCode) { + Navigator.of(context).pop(); //Remove loading dialog + if (statusCode == 201) { + Navigator.of(context).pop(); // pop window + Navigator.of(context).pop(); // pop business profile + Navigator.of(context).pop(); // pop directory + Navigator.of(context).pushNamed( + '/mzansi-directory', + arguments: MzansiDirectoryArguments( + widget.startUpSearch, // startUpSearch + false, // personalSearch + ), + ); + MihAlertServices().successAlert( + "Successfully Added Review!", + "Your review has successfully been added and will now appear under the business.", + context, + ); + } else { + MihAlertServices().errorAlert( + "Error Adding Review", + "There was an error adding your review. Please try again later.", + context, + ); + } + }); + } + } + + String getWindowTitle() { + if (widget.readOnly) { + return "Review Details"; + } else if (widget.businessReview != null) { + return "Update Review"; + } else { + return "Add Review"; + } + } + + @override + void dispose() { + super.dispose(); + _reviewDescriptionController.removeListener(_reviewDescriptionListener); + } + + @override + void initState() { + super.initState(); + _reviewDescriptionListener = () { + setState(() { + _counter.value = _reviewDescriptionController.text.characters.length; + }); + }; + _reviewDescriptionController.addListener(_reviewDescriptionListener); + if (widget.businessReview != null) { + setState(() { + _reviewTitleController.text = widget.businessReview!.rating_title; + _reviewDescriptionController.text = + widget.businessReview!.rating_description; + _reviewScoreController.text = widget.businessReview!.rating_score; + _reviewReviewerController.text = widget.businessReview!.reviewer; + }); + } else { + _reviewScoreController.text = "1.0"; // Default score + } + SuperTokens.getUserId().then((value) { + setState(() { + userId = value; + }); + }); + } + + @override + Widget build(BuildContext context) { + // return const Placeholder(); + return MihPackageWindow( + fullscreen: false, + windowTitle: getWindowTitle(), + onWindowTapClose: () { + Navigator.of(context).pop(); + }, + menuOptions: widget.businessReview != null && !widget.readOnly + ? [ + SpeedDialChild( + child: Icon( + Icons.delete, + color: MzansiInnovationHub.of(context)!.theme.primaryColor(), + ), + label: "Delete Review", + labelBackgroundColor: + MzansiInnovationHub.of(context)!.theme.successColor(), + labelStyle: TextStyle( + color: MzansiInnovationHub.of(context)!.theme.primaryColor(), + fontWeight: FontWeight.bold, + ), + backgroundColor: + MzansiInnovationHub.of(context)!.theme.successColor(), + onTap: () { + showDeleteReviewAlert(); + }, + ), + ] + : null, + windowBody: MihSingleChildScroll( + child: Padding( + padding: + MzansiInnovationHub.of(context)!.theme.screenType == "desktop" + ? EdgeInsets.symmetric(horizontal: widget.screenWidth * 0.05) + : EdgeInsets.symmetric(horizontal: widget.screenWidth * 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + MihForm( + formKey: _formKey, + formFields: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Business Rating", + textAlign: TextAlign.left, + style: TextStyle( + color: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), + widget.readOnly + ? RatingBar.readOnly( + size: 50, + alignment: Alignment.centerLeft, + filledIcon: Icons.star, + emptyIcon: Icons.star_border, + halfFilledIcon: Icons.star_half, + filledColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + emptyColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + halfFilledColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + isHalfAllowed: true, + initialRating: widget.businessReview != null + ? double.parse(_reviewScoreController.text) + : 1, + maxRating: 5, + ) + : RatingBar( + size: 50, + alignment: Alignment.centerLeft, + filledIcon: Icons.star, + emptyIcon: Icons.star_border, + halfFilledIcon: Icons.star_half, + filledColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + emptyColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + halfFilledColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + isHalfAllowed: true, + initialRating: widget.businessReview != null + ? double.parse(_reviewScoreController.text) + : 1, + maxRating: 5, + onRatingChanged: (double) { + setState(() { + _reviewScoreController.text = + double.toStringAsFixed(1); + }); + print(_reviewScoreController.text); + }, + ), + Visibility( + visible: widget.readOnly, + child: const SizedBox(height: 10), + ), + Visibility( + visible: widget.readOnly, + child: MihTextFormField( + // width: 200, + fillColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + inputColor: + MzansiInnovationHub.of(context)!.theme.primaryColor(), + controller: _reviewReviewerController, + multiLineInput: false, + requiredText: true, + readOnly: true, + hintText: "Reviewer", + validator: (value) { + return null; + }, + ), + ), + const SizedBox(height: 10), + MihTextFormField( + // width: 200, + fillColor: + MzansiInnovationHub.of(context)!.theme.secondaryColor(), + inputColor: + MzansiInnovationHub.of(context)!.theme.primaryColor(), + controller: _reviewTitleController, + multiLineInput: false, + requiredText: true, + readOnly: widget.readOnly, + hintText: "Review Title", + validator: (value) { + return MihValidationServices() + .isEmpty(_reviewTitleController.text); + }, + ), + const SizedBox(height: 10), + MihTextFormField( + height: 250, + fillColor: + MzansiInnovationHub.of(context)!.theme.secondaryColor(), + inputColor: + MzansiInnovationHub.of(context)!.theme.primaryColor(), + controller: _reviewDescriptionController, + multiLineInput: true, + requiredText: widget.readOnly, + readOnly: widget.readOnly, + hintText: "Review Description", + validator: (value) { + if (_reviewDescriptionController.text.isEmpty) { + return null; + } else { + return MihValidationServices().validateLength( + _reviewDescriptionController.text, 256); + } + }, + ), + Visibility( + visible: !widget.readOnly, + child: SizedBox( + height: 15, + child: ValueListenableBuilder( + valueListenable: _counter, + builder: + (BuildContext context, int value, Widget? child) { + return Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + "$value", + style: TextStyle( + color: getMissionVisionLimitColor(256), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + Text( + "/256", + style: TextStyle( + color: getMissionVisionLimitColor(256), + fontWeight: FontWeight.bold, + ), + ), + ], + ); + }, + ), + ), + ), + const SizedBox(height: 25), + Visibility( + visible: !widget.readOnly, + child: Center( + child: MihButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + submitForm(); + } else { + MihAlertServices().formNotFilledCompletely(context); + } + }, + buttonColor: MzansiInnovationHub.of(context)! + .theme + .successColor(), + width: 300, + child: Text( + widget.businessReview != null + ? "Update Review" + : "Add Review", + style: TextStyle( + color: MzansiInnovationHub.of(context)! + .theme + .primaryColor(), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/Frontend/lib/mih_packages/mzansi_profile/business_profile/mzansi_business_profile_view.dart b/Frontend/lib/mih_packages/mzansi_profile/business_profile/mzansi_business_profile_view.dart index f70aaefd..fdba7344 100644 --- a/Frontend/lib/mih_packages/mzansi_profile/business_profile/mzansi_business_profile_view.dart +++ b/Frontend/lib/mih_packages/mzansi_profile/business_profile/mzansi_business_profile_view.dart @@ -1,15 +1,16 @@ -import 'package:mzansi_innovation_hub/mih_components/mih_objects/business.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/arguments.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_action.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tools.dart'; import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details_view.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_reviews.dart'; class MzansiBusinessProfileView extends StatefulWidget { - final Business business; + final BusinessViewArguments arguments; const MzansiBusinessProfileView({ super.key, - required this.business, + required this.arguments, }); @override @@ -54,6 +55,11 @@ class _MzansiBusinessProfileViewState extends State { _selcetedIndex = 0; }); }; + temp[const Icon(Icons.star_rate_rounded)] = () { + setState(() { + _selcetedIndex = 1; + }); + }; return MihPackageTools( tools: temp, selcetedIndex: _selcetedIndex, @@ -62,7 +68,11 @@ class _MzansiBusinessProfileViewState extends State { List getToolBody() { List toolBodies = [ - MihBusinessDetailsView(business: widget.business), + MihBusinessDetailsView( + business: widget.arguments.business, + startUpSearch: widget.arguments.startUpSearch, + ), + MihBusinessReviews(business: widget.arguments.business), ]; return toolBodies; } @@ -70,6 +80,7 @@ class _MzansiBusinessProfileViewState extends State { List getToolTitle() { List toolTitles = [ "Profile", + "Reviews", ]; return toolTitles; } diff --git a/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details.dart b/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details.dart index 82c23ea7..1a7c6c1c 100644 --- a/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details.dart +++ b/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details.dart @@ -680,12 +680,18 @@ class _MihBusinessDetailsState extends State { SizedBox( width: 700, child: MihBusinessCard( - businessName: widget.arguments.business!.Name, - cellNumber: widget.arguments.business!.contact_no, - email: widget.arguments.business!.bus_email, - gpsLocation: widget.arguments.business!.gps_location, - //To-Do: Add the business Website - website: widget.arguments.business!.website, + // businessid: widget.arguments.business!.business_id, + // businessName: widget.arguments.business!.Name, + // cellNumber: widget.arguments.business!.contact_no, + // email: widget.arguments.business!.bus_email, + // gpsLocation: widget.arguments.business!.gps_location, + // rating: widget.arguments.business!.rating.isNotEmpty + // ? double.parse(widget.arguments.business!.rating) + // : 0, + // website: widget.arguments.business!.website, + business: widget.arguments.business!, + startUpSearch: null, + width: width, ), ), const SizedBox(height: 30.0), diff --git a/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details_view.dart b/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details_view.dart index b4d7271d..9c34764a 100644 --- a/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details_view.dart +++ b/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_details_view.dart @@ -11,9 +11,11 @@ import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_ class MihBusinessDetailsView extends StatefulWidget { final Business business; + final String? startUpSearch; const MihBusinessDetailsView({ super.key, required this.business, + required this.startUpSearch, }); @override @@ -162,12 +164,18 @@ class _MihBusinessDetailsViewState extends State { SizedBox( width: 700, child: MihBusinessCard( - businessName: widget.business.Name, - cellNumber: widget.business.contact_no, - email: widget.business.bus_email, - gpsLocation: widget.business.gps_location, - //To-Do: Add the business Website - website: widget.business.website, + business: widget.business, + startUpSearch: widget.startUpSearch, + // businessid: widget.business.business_id, + // businessName: widget.business.Name, + // cellNumber: widget.business.contact_no, + // email: widget.business.bus_email, + // gpsLocation: widget.business.gps_location, + // rating: widget.business.rating.isNotEmpty + // ? double.parse(widget.business.rating) + // : 0, + // website: widget.business.website, + width: width, ), ), ], diff --git a/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_reviews.dart b/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_reviews.dart new file mode 100644 index 00000000..dbf8c871 --- /dev/null +++ b/Frontend/lib/mih_packages/mzansi_profile/business_profile/package_tools/mih_business_reviews.dart @@ -0,0 +1,160 @@ +import 'package:custom_rating_bar/custom_rating_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business_review.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_review_business_window.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_mzansi_directory_services.dart'; + +class MihBusinessReviews extends StatefulWidget { + final Business business; + const MihBusinessReviews({ + super.key, + required this.business, + }); + + @override + State createState() => _MihBusinessReviewsState(); +} + +class _MihBusinessReviewsState extends State { + // late Future> _reviews; + + // @override + // void initState() { + // super.initState(); + // _reviews = MihMzansiDirectoryServices().getAllReviewsofBusiness( + // widget.businessId, + // ); + // } + + void onReviewTap(BusinessReview? businessReview, double width) { + // showDialog(context: context, builder: (context)=> ) + showDialog( + context: context, + builder: (context) { + return MihReviewBusinessWindow( + business: widget.business, + businessReview: businessReview, + screenWidth: width, + readOnly: true, + startUpSearch: null, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + double screenWidth = MediaQuery.of(context).size.width; + return FutureBuilder( + future: MihMzansiDirectoryServices().getAllReviewsofBusiness( + widget.business.business_id, + ), + builder: (context, asyncSnapshot) { + if (asyncSnapshot.connectionState == ConnectionState.waiting) { + return const Mihloadingcircle(); + } else if (asyncSnapshot.connectionState == ConnectionState.done && + asyncSnapshot.hasData) { + List reviews = asyncSnapshot.data!; + if (reviews.isEmpty) { + return Column( + children: [ + const SizedBox(height: 50), + Icon( + Icons.star_rate_rounded, + size: 150, + color: + MzansiInnovationHub.of(context)!.theme.secondaryColor(), + ), + Text( + "No reviews yet, be the first the review\n${widget.business.Name}", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } else { + int descriptionDisplayCOunt = 75; + return ListView.separated( + itemCount: reviews.length, + separatorBuilder: (context, index) => Divider(), + itemBuilder: (context, index) { + return ListTile( + onTap: () { + onReviewTap(reviews[index], screenWidth); + }, + title: RatingBar.readOnly( + size: 25, + alignment: Alignment.centerLeft, + filledIcon: Icons.star, + emptyIcon: Icons.star_border, + halfFilledIcon: Icons.star_half, + filledColor: const Color(0xffe9e8a1), + // MzansiInnovationHub.of(context)!.theme.primaryColor(), + emptyColor: MzansiInnovationHub.of(context)! + .theme + .secondaryColor(), + halfFilledColor: const Color(0xffe9e8a1), + // MzansiInnovationHub.of(context)!.theme.primaryColor(), + // filledColor: + // MzansiInnovationHub.of(context)!.theme.secondaryColor(), + // emptyColor: + // MzansiInnovationHub.of(context)!.theme.secondaryColor(), + // halfFilledColor: + // MzansiInnovationHub.of(context)!.theme.secondaryColor(), + isHalfAllowed: true, + initialRating: double.parse(reviews[index].rating_score), + maxRating: 5, + ), + subtitle: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + // Text( + // "${reviews[index].reviewer} ", + // style: TextStyle( + // fontSize: 15, + // fontWeight: FontWeight.bold, + // ), + // ), + Text( + reviews[index].rating_title, + softWrap: true, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + "${reviews[index].rating_description.substring(0, reviews[index].rating_description.length >= descriptionDisplayCOunt ? descriptionDisplayCOunt : reviews[index].rating_description.length - 1)}${reviews[index].rating_description.length >= descriptionDisplayCOunt ? "..." : ""}", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.normal, + ), + ), + Text( + "${reviews[index].date_time.split("T")[0]} ", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ); + }, + ); + } + } else { + return Center(child: Text('Error')); + } + }); + } +} diff --git a/Frontend/lib/mih_services/mih_alert_services.dart b/Frontend/lib/mih_services/mih_alert_services.dart index 431834cc..35157e4b 100644 --- a/Frontend/lib/mih_services/mih_alert_services.dart +++ b/Frontend/lib/mih_services/mih_alert_services.dart @@ -55,4 +55,66 @@ class MihAlertServices { }, ); } + + void successAlert(String title, String message, BuildContext context) { + showDialog( + context: context, + builder: (context) { + return MihPackageAlert( + alertIcon: Icon( + Icons.check_circle_outline_rounded, + size: 150, + color: MzansiInnovationHub.of(context)!.theme.successColor(), + ), + alertTitle: title, + alertBody: Column( + children: [ + Text( + message, + style: TextStyle( + color: + MzansiInnovationHub.of(context)!.theme.secondaryColor(), + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 25), + ], + ), + alertColour: MzansiInnovationHub.of(context)!.theme.successColor(), + ); + }, + ); + } + + void errorAlert(String title, String message, BuildContext context) { + showDialog( + context: context, + builder: (context) { + return MihPackageAlert( + alertIcon: Icon( + Icons.warning_amber_rounded, + size: 150, + color: MzansiInnovationHub.of(context)!.theme.errorColor(), + ), + alertTitle: title, + alertBody: Column( + children: [ + Text( + message, + style: TextStyle( + color: + MzansiInnovationHub.of(context)!.theme.secondaryColor(), + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 25), + ], + ), + alertColour: MzansiInnovationHub.of(context)!.theme.errorColor(), + ); + }, + ); + } } diff --git a/Frontend/lib/mih_services/mih_mzansi_directory_services.dart b/Frontend/lib/mih_services/mih_mzansi_directory_services.dart new file mode 100644 index 00000000..0a886723 --- /dev/null +++ b/Frontend/lib/mih_services/mih_mzansi_directory_services.dart @@ -0,0 +1,128 @@ +import 'dart:convert'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/business_review.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_env.dart'; +import 'package:supertokens_flutter/http.dart' as http; + +class MihMzansiDirectoryServices { + final baseAPI = AppEnviroment.baseApiUrl; + + Future getUserReviewOfBusiness( + String app_id, + String business_id, + ) async { + final response = await http.get(Uri.parse( + "${AppEnviroment.baseApiUrl}/mzansi-directory/business-ratings/user/$app_id/$business_id")); + print(response.statusCode); + if (response.statusCode == 200) { + String body = response.body; + var jsonBody = jsonDecode(body); + BusinessReview? busRev = BusinessReview.fromJson(jsonBody); + return busRev; + } else { + return null; + } + } + + Future> getAllReviewsofBusiness( + String business_id, + ) async { + final response = await http.get(Uri.parse( + "${AppEnviroment.baseApiUrl}/mzansi-directory/business-ratings/all/$business_id")); + if (response.statusCode == 200) { + Iterable l = jsonDecode(response.body); + List businessReviews = List.from( + l.map((model) => BusinessReview.fromJson(model))); + return businessReviews; + } else { + throw Exception('failed to fetch Business Reviews'); + } + } + + Future addBusinessReview( + String app_id, + String business_id, + String rating_title, + String rating_description, + String rating_score, + String current_rating, + ) async { + var response = await http.post( + Uri.parse( + "${AppEnviroment.baseApiUrl}/mzansi-directory/business-rating/insert/"), + headers: { + "Content-Type": "application/json; charset=UTF-8" + }, + body: jsonEncode({ + "app_id": app_id, + "business_id": business_id, + "rating_title": rating_title, + "rating_description": rating_description, + "rating_score": rating_score, + "current_rating": current_rating, + }), + ); + if (response.statusCode == 201) { + return response.statusCode; + } else { + return response.statusCode; + } + } + + Future deleteBusinessReview( + int idbusiness_ratings, + String business_id, + String rating_score, + String current_rating, + ) async { + var response = await http.delete( + Uri.parse( + "${AppEnviroment.baseApiUrl}/mzansi-directory/business-rating/delete/"), + headers: { + "Content-Type": "application/json; charset=UTF-8" + }, + body: jsonEncode({ + "idbusiness_ratings": idbusiness_ratings, + "business_id": business_id, + "rating_score": rating_score, + "current_rating": current_rating, + }), + ); + if (response.statusCode == 200) { + return response.statusCode; + } else { + return response.statusCode; + } + } + + Future updateBusinessReview( + int idbusiness_ratings, + String business_id, + String rating_title, + String rating_description, + String rating_new_score, + String rating_old_score, + String current_rating, + ) async { + var response = await http.put( + Uri.parse( + "${AppEnviroment.baseApiUrl}/mzansi-directory/business-rating/update/"), + headers: { + "Content-Type": "application/json; charset=UTF-8" + }, + body: jsonEncode({ + "idbusiness_ratings": idbusiness_ratings, + "business_id": business_id, + "rating_title": rating_title, + "rating_description": rating_description, + "rating_new_score": rating_new_score, + "rating_old_score": rating_old_score, + "current_rating": current_rating, + }), + ); + if (response.statusCode == 200) { + return response.statusCode; + } else { + return response.statusCode; + } + } +} diff --git a/Frontend/pubspec.lock b/Frontend/pubspec.lock index e08b9f6e..74f9c57b 100644 --- a/Frontend/pubspec.lock +++ b/Frontend/pubspec.lock @@ -265,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + custom_rating_bar: + dependency: "direct main" + description: + name: custom_rating_bar + sha256: c2dafa488843b29f50ac2c2905b6a4cefd2cb603de31c6224adb047b6ed8593e + url: "https://pub.dev" + source: hosted + version: "3.0.0" dart_style: dependency: transitive description: diff --git a/Frontend/pubspec.yaml b/Frontend/pubspec.yaml index e07bea1f..ba69a30e 100644 --- a/Frontend/pubspec.yaml +++ b/Frontend/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: pwa_install: ^0.0.6 google_mobile_ads: ^6.0.0 redacted: ^1.0.13 + custom_rating_bar: ^3.0.0 dev_dependencies: flutter_test: diff --git a/backend/.gitignore b/backend/.gitignore index 377f7991..156d46ab 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,3 @@ -database/__pycache__/ +.env __pycache__/ temp*.pdf \ No newline at end of file diff --git a/backend/Minio_Storage/minioConnection.py b/backend/Minio_Storage/minioConnection.py index 0ec200a1..23e116aa 100644 --- a/backend/Minio_Storage/minioConnection.py +++ b/backend/Minio_Storage/minioConnection.py @@ -1,19 +1,25 @@ from minio import Minio +import os +from dotenv import load_dotenv + +load_dotenv() +minioAccess = os.getenv("MINIO_ACCESS_KEY") +minioSecret = os.getenv("MINIO_SECRET_KEY") def minioConnect(env): if(env == "Dev"): return Minio( "minio:9000", # "minio.mzansi-innovation-hub.co.za", - access_key="0RcgutfvcDq28lz7", - secret_key="nEED72ZlKYgDqH9Iy46fVGGT9TfabGWO", + access_key=minioAccess, + secret_key=minioSecret, secure=False ) else: return Minio( #"minio:9000", "minio.mzansi-innovation-hub.co.za", - access_key="0RcgutfvcDq28lz7", - secret_key="nEED72ZlKYgDqH9Iy46fVGGT9TfabGWO", + access_key=minioAccess, + secret_key=minioSecret, secure=True ) \ No newline at end of file diff --git a/backend/database/dbConnection.py b/backend/database/dbConnection.py index fc16e946..04c8a118 100644 --- a/backend/database/dbConnection.py +++ b/backend/database/dbConnection.py @@ -1,48 +1,62 @@ import mysql.connector +import os +from dotenv import load_dotenv + +load_dotenv() +dbUser = os.getenv("DB_USER") +dbPass = os.getenv("DB_PASSWD") def dbPatientManagerConnect(): return mysql.connector.connect( host="mysqldb", - user="root", - passwd="C@rtoon1995", + user=dbUser, + passwd=dbPass, database="patient_manager" ) def dbAppDataConnect(): return mysql.connector.connect( host="mysqldb", - user="root", - passwd="C@rtoon1995", + user=dbUser, + passwd=dbPass, database="app_data" ) def dbDataAccessConnect(): return mysql.connector.connect( host="mysqldb", - user="root", - passwd="C@rtoon1995", + user=dbUser, + passwd=dbPass, database="data_access" ) def dbMzansiWalletConnect(): return mysql.connector.connect( host="mysqldb", - user="root", - passwd="C@rtoon1995", + user=dbUser, + passwd=dbPass, database="mzansi_wallet" ) +def dbMzansiDirectoryConnect(): + return mysql.connector.connect( + host="mysqldb", + user=dbUser, + passwd=dbPass, + database="mzansi_directory" + ) + def dbMzansiCalendarConnect(): return mysql.connector.connect( host="mysqldb", - user="root", - passwd="C@rtoon1995", + user=dbUser, + passwd=dbPass, database="mzansi_calendar" ) def dbAllConnect(): return mysql.connector.connect( host="mysqldb", - user="root", - passwd="C@rtoon1995", + user=dbUser, + passwd=dbPass, ) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index ec371cf4..f542a1f4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -17,6 +17,7 @@ import routers.business as business import routers.access_request as access_request import routers.patient_access as patient_access import routers.mzansi_wallet as mzansi_wallet +import routers.mzansi_directory as mzansi_directory import routers.icd10_codes as icd10_codes from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware import Middleware @@ -92,6 +93,7 @@ app.include_router(business_user.router) app.include_router(business.router) app.include_router(notifications.router) app.include_router(mzansi_wallet.router) +app.include_router(mzansi_directory.router) app.include_router(icd10_codes.router) app.include_router(appointments.router) diff --git a/backend/requirements.txt b/backend/requirements.txt index d0594fd5..26035ff9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,5 +6,6 @@ reportlab requests watchfiles python-multipart +python-dotenv xlrd supertokens-python==0.29.2 \ No newline at end of file diff --git a/backend/routers/business.py b/backend/routers/business.py index 232c523d..7ea28092 100644 --- a/backend/routers/business.py +++ b/backend/routers/business.py @@ -72,9 +72,10 @@ async def read_all_businesses(search: str, session: SessionContainer = Depends(v query += "practice_no, vat_no, " query += "website, rating, mission_vision " query += "FROM business " - query += "WHERE LOWER(business.Name) LIKE %s OR LOWER(business.type) LIKE %s" + query += "WHERE LOWER(business.Name) LIKE %s OR LOWER(business.type) LIKE %s " + query += "OR LOWER(business.bus_email) LIKE %s OR LOWER(business.mission_vision) LIKE %s" search_term = f"%{search.lower()}%" # Add wildcards and lowercase - cursor.execute(query, (search_term, search_term)) + cursor.execute(query, (search_term, search_term, search_term, search_term)) items = [ { "business_id": item[0], diff --git a/backend/routers/mzansi_directory.py b/backend/routers/mzansi_directory.py new file mode 100644 index 00000000..1ad55797 --- /dev/null +++ b/backend/routers/mzansi_directory.py @@ -0,0 +1,236 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from datetime import datetime +import database +from supertokens_python.recipe.session.framework.fastapi import verify_session +from supertokens_python.recipe.session import SessionContainer +from fastapi import Depends + +router = APIRouter() + +# class BusinessRatingUserGet(BaseModel): +# app_id: str +# business_id: str + +class BusinessRatingInsertRequest(BaseModel): + app_id: str + business_id: str + rating_title: str + rating_description: str + rating_score: str + current_rating: str + +class BusinessRatingDeleteRequest(BaseModel): + idbusiness_ratings: int + business_id: str + rating_score: str + current_rating: str + +class BusinessRatingUpdateRequest(BaseModel): + idbusiness_ratings: int + business_id: str + rating_title: str + rating_description: str + rating_new_score: str + rating_old_score: str + current_rating: str + +@router.get("/mzansi-directory/business-ratings/user/{app_id}/{business_id}", tags=["Mzansi Directory"]) +async def read_all_ratings_by_business_id(app_id: str,business_id: str, session: SessionContainer = Depends(verify_session())): # , session: SessionContainer = Depends(verify_session()) + db = database.dbConnection.dbAllConnect() + cursor = db.cursor() + query = "" + query += "SELECT business_ratings.idbusiness_ratings, business_ratings.app_id, business_ratings.business_id, " + query += "business_ratings.rating_title, business_ratings.rating_description, business_ratings.rating_score, " + query += "business_ratings.date_time, users.username as 'reviewer' " + query += "FROM mzansi_directory.business_ratings " + query += "inner join app_data.users " + query += "on business_ratings.app_id = users.app_id " + query += "where business_ratings.business_id = %s and business_ratings.app_id = %s;" + cursor.execute(query, (business_id, + app_id,)) + item = cursor.fetchone() # Get only one row + cursor.close() + db.close() + + if item: + # Return a single dictionary + return { + "idbusiness_ratings": item[0], + "app_id": item[1], + "business_id": item[2], + "rating_title": item[3], + "rating_description": item[4], + "rating_score": item[5], + "date_time": item[6], + "reviewer": item[7], + } + else: + # Return an empty response or a specific message + return None + # items = [ + # { + # "idbusiness_ratings": item[0], + # "app_id": item[1], + # "business_id": item[2], + # "rating_title": item[3], + # "rating_description": item[4], + # "rating_score": item[5], + # "date_time": item[6], + # "reviewer": item[7], + # } + # for item in cursor.fetchall() + # ] + # cursor.close() + # db.close() + # return items[0] + +@router.get("/mzansi-directory/business-ratings/all/{business_id}", tags=["Mzansi Directory"]) +async def read_all_ratings_by_business_id(business_id: str, session: SessionContainer = Depends(verify_session())): # , session: SessionContainer = Depends(verify_session()) + db = database.dbConnection.dbAllConnect() + cursor = db.cursor() + query = "" + query += "SELECT business_ratings.idbusiness_ratings, business_ratings.app_id, business_ratings.business_id, " + query += "business_ratings.rating_title, business_ratings.rating_description, business_ratings.rating_score, " + query += "business_ratings.date_time, users.username as 'reviewer' " + query += "FROM mzansi_directory.business_ratings " + query += "inner join app_data.users " + query += "on business_ratings.app_id = users.app_id " + query += "where business_ratings.business_id = %s " + query += "order by business_ratings.date_time desc;" + cursor.execute(query, (business_id,)) + items = [ + { + "idbusiness_ratings": item[0], + "app_id": item[1], + "business_id": item[2], + "rating_title": item[3], + "rating_description": item[4], + "rating_score": item[5], + "date_time": item[6], + "reviewer": item[7], + } + for item in cursor.fetchall() + ] + cursor.close() + db.close() + return items + +@router.post("/mzansi-directory/business-rating/insert/", tags=["Mzansi Directory"], status_code=201) +async def insert_loyalty_card(itemRequest : BusinessRatingInsertRequest): #, session: SessionContainer = Depends(verify_session()) + db = database.dbConnection.dbAllConnect() + nowDateTime = datetime.now() + formatedDateTime = nowDateTime.strftime("%Y-%m-%d %H:%M:%S") + cursor = db.cursor() + try: + # Get No Of reviews for business + businessReviewCountQuery = "select count(*) from mzansi_directory.business_ratings where business_ratings.business_id = %s" + countData = (itemRequest.business_id,) + cursor.execute(businessReviewCountQuery, countData) + countResult = cursor.fetchone() + row_count = countResult[0] if countResult else 0 + print(f"Number of rows in business_ratings: {row_count}") + # add business rating + addQuery = "insert into mzansi_directory.business_ratings " + addQuery += "(business_ratings.app_id, business_ratings.business_id, business_ratings.rating_title, business_ratings.rating_description, business_ratings.rating_score, business_ratings.date_time) " + addQuery += "values (%s, %s, %s, %s, %s, %s)" + addQueryData = (itemRequest.app_id, + itemRequest.business_id, + itemRequest.rating_title, + itemRequest.rating_description, + itemRequest.rating_score, + formatedDateTime, + ) + cursor.execute(addQuery, addQueryData) + # Calc New Rating and update business rating + newRating = ((float(itemRequest.current_rating) * row_count) + float(itemRequest.rating_score)) / (row_count + 1) + print(f"New Rating: {newRating}") + updateBusinessQuery = "update app_data.business " + updateBusinessQuery += "set rating = %s " + updateBusinessQuery += "where business_id = %s" + updateBusinessData = (newRating, itemRequest.business_id) + cursor.execute(updateBusinessQuery, updateBusinessData) + db.commit() + except Exception as error: + print(error) + raise HTTPException(status_code=404, detail="Failed to Create Record") + # return {"message": error} + cursor.close() + db.close() + return {"message": "Successfully Created Record"} + +@router.delete("/mzansi-directory/business-rating/delete/", tags=["Mzansi Directory"]) +async def Delete_loyalty_card(itemRequest : BusinessRatingDeleteRequest, session: SessionContainer = Depends(verify_session())): #, session: SessionContainer = Depends(verify_session()) + db = database.dbConnection.dbAllConnect() + cursor = db.cursor() + try: + # Get No Of reviews for business + businessReviewCountQuery = "select count(*) from mzansi_directory.business_ratings where business_ratings.business_id = %s" + countData = (itemRequest.business_id,) + cursor.execute(businessReviewCountQuery, countData) + countResult = cursor.fetchone() + row_count = countResult[0] if countResult else 0 + print(f"Number of rows in business_ratings: {row_count}") + # Delete business rating + query = "delete from mzansi_directory.business_ratings " + query += "where business_ratings.idbusiness_ratings=%s" + cursor.execute(query, (str(itemRequest.idbusiness_ratings),)) + # Calc New Rating and update business rating + if(row_count <= 1): + newRating = 0.0 + else: + newRating = ((float(itemRequest.current_rating) * row_count) - float(itemRequest.rating_score)) / (row_count - 1) + print(f"New Rating: {newRating}") + updateBusinessQuery = "update app_data.business " + updateBusinessQuery += "set rating = %s " + updateBusinessQuery += "where business_id = %s" + updateBusinessData = (newRating, itemRequest.business_id) + cursor.execute(updateBusinessQuery, updateBusinessData) + db.commit() + except Exception as error: + print(error) + raise HTTPException(status_code=404, detail="Failed to Delete Record") + cursor.close() + db.close() + return {"message": "Successfully deleted Record"} + +@router.put("/mzansi-directory/business-rating/update/", tags=["Mzansi Directory"]) +async def UpdatePatient(itemRequest : BusinessRatingUpdateRequest, session: SessionContainer = Depends(verify_session())): + db = database.dbConnection.dbMzansiDirectoryConnect() + cursor = db.cursor() + nowDateTime = datetime.now() + formatedDateTime = nowDateTime.strftime("%Y-%m-%d %H:%M:%S") + try: + # Get No Of reviews for business + businessReviewCountQuery = "select count(*) from mzansi_directory.business_ratings where business_ratings.business_id = %s" + countData = (itemRequest.business_id,) + cursor.execute(businessReviewCountQuery, countData) + countResult = cursor.fetchone() + row_count = countResult[0] if countResult else 0 + print(f"Number of rows in business_ratings: {row_count}") + # Update business rating + query = "update business_ratings " + query += "set rating_title=%s, rating_description=%s, rating_score=%s, date_time=%s " + query += "where idbusiness_ratings=%s" + notetData = (itemRequest.rating_title, + itemRequest.rating_description, + itemRequest.rating_new_score, + formatedDateTime, + itemRequest.idbusiness_ratings, + ) + cursor.execute(query, notetData) + # Calc New Rating and update business rating + # add new rating and old rating params + newRating = ((float(itemRequest.current_rating) * row_count) - float(itemRequest.rating_old_score) + float(itemRequest.rating_new_score)) / (row_count) + print(f"New Rating: {newRating}") + updateBusinessQuery = "update app_data.business " + updateBusinessQuery += "set rating = %s " + updateBusinessQuery += "where business_id = %s" + updateBusinessData = (newRating, itemRequest.business_id) + cursor.execute(updateBusinessQuery, updateBusinessData) + db.commit() + except Exception as error: + raise HTTPException(status_code=404, detail="Failed to Update Record") + cursor.close() + db.close() + return {"message": "Successfully Updated Record"} \ No newline at end of file diff --git a/backend/routers/users.py b/backend/routers/users.py index b4938efb..9d9a44c0 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -74,10 +74,12 @@ async def read_all_users(search: str, session: SessionContainer = Depends(verify cursor = db.cursor() query = "" query += "SELECT * FROM users " - query += "WHERE (LOWER(email) LIKE %s OR LOWER(username) LIKE %s) " + query += "WHERE (LOWER(email) LIKE %s OR LOWER(username) LIKE %s " + query += "OR LOWER(fname) LIKE %s OR LOWER(lname) LIKE %s " + query += "OR LOWER(purpose) LIKE %s) " query += "AND username != ''" search_term = f"%{search.lower()}%" # Add wildcards and lowercase - cursor.execute(query, (search_term, search_term)) + cursor.execute(query, (search_term, search_term,search_term, search_term, search_term)) items = [ { "idUser": item[0],