diff --git a/lib/app_constants.dart b/lib/app_constants.dart index 7f1013d3e..4a46b35d0 100644 --- a/lib/app_constants.dart +++ b/lib/app_constants.dart @@ -42,6 +42,7 @@ class RoutePaths { static const String ParkingLotsView = "parking/parking_lots_view"; static const String NeighborhoodsView = "parking/neighborhoods_view"; static const String NeighborhoodsLotsView = "parking/neighborhoods_lot_view"; + static const String AvailabilityDetailedView = "availability/detailed_view"; } class RouteTitles { @@ -70,6 +71,7 @@ class RouteTitles { 'dining/dining_list_view': 'Dining', 'dining/dining_detail_view': 'Dining', 'dining/dining_nutrition_view': 'Dining', + 'availability/detailed_view': 'Availability' }; } diff --git a/lib/app_router.dart b/lib/app_router.dart index 0ebcebefb..aa5aa8021 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -1,9 +1,11 @@ import 'package:campus_mobile_experimental/app_constants.dart'; +import 'package:campus_mobile_experimental/core/models/availability.dart'; import 'package:campus_mobile_experimental/core/models/dining.dart'; import 'package:campus_mobile_experimental/core/models/dining_menu.dart'; import 'package:campus_mobile_experimental/core/models/events.dart'; import 'package:campus_mobile_experimental/core/models/news.dart'; import 'package:campus_mobile_experimental/ui/availability/manage_availability_view.dart'; +import 'package:campus_mobile_experimental/ui/availability/availability_detail_view.dart'; import 'package:campus_mobile_experimental/ui/classes/classes_list.dart'; import 'package:campus_mobile_experimental/ui/dining/dining_detail_view.dart'; import 'package:campus_mobile_experimental/ui/dining/dining_list.dart'; @@ -99,6 +101,12 @@ class Router { Provider.of(_).changeTitle(settings.name); return ManageAvailabilityView(); }); + case RoutePaths.AvailabilityDetailedView: + SubLocations subLocation = settings.arguments as SubLocations; + return MaterialPageRoute(builder: (_) { + Provider.of(_).changeTitle(settings.name); + return AvailabilityDetailedView(subLocation: subLocation); + }); case RoutePaths.DiningViewAll: return MaterialPageRoute(builder: (_) { Provider.of(_).changeTitle(settings.name); diff --git a/lib/core/models/availability.dart b/lib/core/models/availability.dart index 487f1fc6c..94c2f2687 100644 --- a/lib/core/models/availability.dart +++ b/lib/core/models/availability.dart @@ -1,48 +1,129 @@ +// To parse this JSON data, do +// +// final availabilityStatus = availabilityStatusFromJson(jsonString); + import 'dart:convert'; -List availabilityModelFromJson(String str) { - var jsonStr = json.decode(str)['data']['children']; - return List.from( - jsonStr.map((x) => AvailabilityModel.fromJson(x))); +AvailabilityStatus availabilityStatusFromJson(String str) => + AvailabilityStatus.fromJson(json.decode(str)); + +String availabilityStatusToJson(AvailabilityStatus data) => + json.encode(data.toJson()); + +class AvailabilityStatus { + AvailabilityStatus({ + this.status, + this.data, + this.timestamp, + }); + + String? status; + List? data; + DateTime? timestamp; + + factory AvailabilityStatus.fromJson(Map json) => + AvailabilityStatus( + status: json["status"] == null ? null : json["status"], + data: json["data"] == null + ? null + : List.from( + json["data"].map((x) => AvailabilityModel.fromJson(x))), + timestamp: json["timestamp"] == null + ? null + : DateTime.parse(json["timestamp"]), + ); + + Map toJson() => { + "status": status == null ? null : status, + "data": data == null + ? null + : List.from(data!.map((x) => x.toJson())), + "timestamp": timestamp == null ? null : timestamp!.toIso8601String(), + }; } class AvailabilityModel { - int? locationId; - bool? isOpen; - bool? isError; - double? percent; - String? locationName; - List? subLocations; - - AvailabilityModel( - {this.locationId, - this.isOpen, - this.isError, - this.percent, - this.locationName, - this.subLocations}); + AvailabilityModel({ + this.id, + this.name, + this.subLocations, + }); + + int? id; + String? name; + List? subLocations; factory AvailabilityModel.fromJson(Map json) => AvailabilityModel( - locationId: json["id"] == null ? null : json["id"], - isOpen: json["isOpen"] == null ? null : json["isOpen"], - isError: json["isError"] == null ? null : json["isError"], - locationName: json["name"] == null ? null : json["name"], - percent: json["percent"] == null ? null : json["percent"].toDouble(), - subLocations: json["children"] == null + id: json["id"] == null ? null : json["id"], + name: json["name"] == null ? null : json["name"], + subLocations: json["childCounts"] == null ? null - : List.from( - json["children"].map((x) => AvailabilityModel.fromJson(x))), + : List.from( + json["childCounts"].map((x) => SubLocations.fromJson(x))), ); Map toJson() => { - "locationId": locationId == null ? null : locationId, - "locationName": locationName == null ? null : locationName, - "isOpen": isOpen == null ? null : isOpen, - "isError": isError == null ? null : isError, - "percent": percent == null ? null : percent.toString(), - "children": subLocations == null + "id": id == null ? null : id, + "name": name == null ? null : name, + "childCounts": subLocations == null ? null : List.from(subLocations!.map((x) => x.toJson())), }; } + +class SubLocations { + SubLocations( + {this.id, this.name, this.percentage, this.isActive, this.floors}); + + int? id; + String? name; + double? percentage; + bool? isActive; + List? floors; + + factory SubLocations.fromJson(Map json) => SubLocations( + id: json["id"] == null ? null : json["id"], + name: json["name"] == null ? null : json["name"], + percentage: + json["percentage"] == null ? null : json["percentage"].toDouble(), + isActive: json["isActive"] == null ? null : json["isActive"], + floors: json["childCounts"] == null + ? null + : List.from( + json["childCounts"].map((x) => Floor.fromJson(x)))); + + Map toJson() => { + "id": id == null ? null : id, + "name": name == null ? null : name, + "percentage": percentage == null ? null : percentage, + "isActive": isActive == null ? null : isActive, + "floors": floors == null ? null : floors + }; +} + +class Floor { + Floor({this.id, this.name, this.count, this.percentage, this.isActive}); + + int? id; + String? name; + int? count; + double? percentage; + bool? isActive; + + factory Floor.fromJson(Map json) => Floor( + id: json["id"] == null ? null : json["id"], + name: json["name"] == null ? null : json["name"], + count: json["count"] == null ? null : json["count"], + percentage: + json["percentage"] == null ? null : json["percentage"].toDouble(), + isActive: json["isActive"] == null ? null : json["isActive"], + ); + + Map toJson() => { + "id": id == null ? null : id, + "name": name == null ? null : name, + "percentage": percentage == null ? null : percentage, + "isActive": isActive == null ? null : isActive, + }; +} diff --git a/lib/core/providers/availability.dart b/lib/core/providers/availability.dart index 4a15c4428..0dba8a0f8 100644 --- a/lib/core/providers/availability.dart +++ b/lib/core/providers/availability.dart @@ -1,4 +1,3 @@ -import 'package:campus_mobile_experimental/app_constants.dart'; import 'package:campus_mobile_experimental/core/models/availability.dart'; import 'package:campus_mobile_experimental/core/providers/user.dart'; import 'package:campus_mobile_experimental/core/services/availability.dart'; @@ -45,20 +44,20 @@ class AvailabilityDataProvider extends ChangeNotifier { if (await _availabilityService.fetchData()) { /// setting the LocationViewState based on user data for (AvailabilityModel model in _availabilityService.data!) { - newMapOfLots[model.locationName] = model; + newMapOfLots[model.name] = model; /// if the user is logged out and has not put any preferences, /// show all locations by default if (_userDataProvider .userProfileModel!.selectedOccuspaceLocations!.isEmpty) { - locationViewState[model.locationName] = true; + locationViewState[model.name] = true; } /// otherwise, LocationViewState should be true for all selectedOccuspaceLocations else { - _locationViewState[model.locationName] = _userDataProvider + _locationViewState[model.name] = _userDataProvider .userProfileModel!.selectedOccuspaceLocations! - .contains(model.locationName); + .contains(model.name); } } @@ -70,12 +69,6 @@ class AvailabilityDataProvider extends ChangeNotifier { _userDataProvider.userProfileModel!.selectedOccuspaceLocations); _lastUpdated = DateTime.now(); } else { - if (_error != null && - _error!.contains(ErrorConstants.invalidBearerToken)) { - if (await _availabilityService.getNewToken()) { - fetchAvailability(); - } - } _error = _availabilityService.error; } _isLoading = false; @@ -164,7 +157,7 @@ class AvailabilityDataProvider extends ChangeNotifier { List locationsToReturn = []; for (AvailabilityModel model in _availabilityModels as Iterable? ?? []) { - locationsToReturn.add(model.locationName); + locationsToReturn.add(model.name); } return locationsToReturn; } diff --git a/lib/core/services/availability.dart b/lib/core/services/availability.dart index 9025b1386..482195f4f 100644 --- a/lib/core/services/availability.dart +++ b/lib/core/services/availability.dart @@ -15,63 +15,32 @@ class AvailabilityService { List? get data => _data; final NetworkHelper _networkHelper = NetworkHelper(); - final Map headers = { - "accept": "application/json", - }; - final String endpoint = - "https://api-qa.ucsd.edu:8243/occuspace/v2.0/busyness"; Future fetchData() async { _error = null; _isLoading = true; try { /// fetch data - String _response = - await (_networkHelper.authorizedFetch(endpoint, headers)); + String _response = await (_networkHelper.authorizedFetch( + "https://api-qa.ucsd.edu:8243/campusbusyness/v1/busyness", { + "Authorization": + "Basic djJlNEpYa0NJUHZ5akFWT0VRXzRqZmZUdDkwYTp2emNBZGFzZWpmaWZiUDc2VUJjNDNNVDExclVh" + })); /// parse data - final data = availabilityModelFromJson(_response); - _isLoading = false; + final data = availabilityStatusFromJson(_response); - _data = data; - return true; - } catch (e) { - /// if the authorized fetch failed we know we have to refresh the - /// token for this service - if (e.toString().contains("401")) { - if (await getNewToken()) { - return await fetchData(); - } - } - _error = e.toString(); _isLoading = false; - return false; - } - } - - Future getNewToken() async { - final String tokenEndpoint = "https://api-qa.ucsd.edu:8243/token"; - final Map tokenHeaders = { - "content-type": 'application/x-www-form-urlencoded', - "Authorization": - "Basic djJlNEpYa0NJUHZ5akFWT0VRXzRqZmZUdDkwYTp2emNBZGFzZWpmaWZiUDc2VUJjNDNNVDExclVh" - }; - try { - var response = await _networkHelper.authorizedPost( - tokenEndpoint, tokenHeaders, "grant_type=client_credentials"); - - headers["Authorization"] = "Bearer " + response["access_token"]; - + _data = data.data; return true; } catch (e) { _error = e.toString(); + _isLoading = false; return false; } } bool get isLoading => _isLoading; - String? get error => _error; - DateTime? get lastUpdated => _lastUpdated; } diff --git a/lib/core/services/user.dart b/lib/core/services/user.dart index 73f42ce5f..9b8a77de3 100644 --- a/lib/core/services/user.dart +++ b/lib/core/services/user.dart @@ -13,6 +13,8 @@ class UserProfileService { final String _endpoint = 'https://api-qa.ucsd.edu:8243/mp-registration/1.0.0'; Future downloadUserProfile(Map headers) async { + print("user headers:"); + print(headers.toString()); _error = null; _isLoading = true; try { diff --git a/lib/ui/availability/availability_card.dart b/lib/ui/availability/availability_card.dart index b8c99d999..d3ffe6355 100644 --- a/lib/ui/availability/availability_card.dart +++ b/lib/ui/availability/availability_card.dart @@ -2,6 +2,7 @@ import 'package:campus_mobile_experimental/app_constants.dart'; import 'package:campus_mobile_experimental/core/models/availability.dart'; import 'package:campus_mobile_experimental/core/providers/availability.dart'; import 'package:campus_mobile_experimental/core/providers/cards.dart'; +import 'package:campus_mobile_experimental/ui/availability/availability_constants.dart'; import 'package:campus_mobile_experimental/ui/availability/availability_display.dart'; import 'package:campus_mobile_experimental/ui/common/card_container.dart'; import 'package:campus_mobile_experimental/ui/common/dots_indicator.dart'; @@ -43,16 +44,39 @@ class _AvailabilityCardState extends State { Widget buildAvailabilityCard(List data) { List locationsList = []; + + // loop through all the models, adding each one to locationsList for (AvailabilityModel? model in data) { if (model != null) { - if (_availabilityDataProvider.locationViewState[model.locationName]!) { - locationsList.add(AvailabilityDisplay( - model: model, - )); + if (_availabilityDataProvider.locationViewState[model.name]!) { + locationsList.add(AvailabilityDisplay(model: model)); } } } + // the user chose no location, so instead show "No Location to Display" + if (locationsList.length == 0) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: Text( + "No Location to Display", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: LOCATION_FONT_SIZE, + ), + ), + padding: EdgeInsets.only( + bottom: TITLE_BOTTOM_PADDING, + ), + ), + Text("Add Locations via 'Manage Locations'"), + ], + ); + } + return Column( children: [ Flexible( @@ -91,4 +115,4 @@ class _AvailabilityCardState extends State { )); return actionButtons; } -} +} \ No newline at end of file diff --git a/lib/ui/availability/availability_constants.dart b/lib/ui/availability/availability_constants.dart new file mode 100644 index 000000000..6006772a7 --- /dev/null +++ b/lib/ui/availability/availability_constants.dart @@ -0,0 +1,9 @@ +/// Constant Values +const double LOCATION_FONT_SIZE = 17; +const double PROGRESS_BAR_HEIGHT = 12; +const double PROGRESS_BAR_WIDTH = 325; +const double TITLE_SIDE_PADDINGS = 16; +const double TITLE_BOTTOM_PADDING = 5; +const double BORDER_RADIUS = 15; +const double DATA_UNAVAILABLE_TOP_PADDING = 10; +const int BACKGROUND_GREY_SHADE = 200; \ No newline at end of file diff --git a/lib/ui/availability/availability_detail_view.dart b/lib/ui/availability/availability_detail_view.dart new file mode 100644 index 000000000..42c7cb4b6 --- /dev/null +++ b/lib/ui/availability/availability_detail_view.dart @@ -0,0 +1,93 @@ +import 'package:campus_mobile_experimental/core/models/availability.dart'; +import 'package:campus_mobile_experimental/ui/common/container_view.dart'; +import 'package:campus_mobile_experimental/ui/availability/availability_constants.dart'; +import 'package:flutter/material.dart'; + +class AvailabilityDetailedView extends StatelessWidget { + final SubLocations subLocation; + const AvailabilityDetailedView({required this.subLocation}); + + @override + Widget build(BuildContext context) { + return ContainerView( + child: buildLocationsList(context, subLocation), + ); + } + + Widget buildLocationsList(BuildContext context, subLocation) { + // Add a tile for the subLocation name + List list = []; + list.add(ListTile( + title: Text( + "${subLocation.name}", + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 24, + fontWeight: FontWeight.bold), + ), + )); + + // Add a tile for every floor in the subLocation list + for (int i = 0; i < subLocation.floors.length; i++) { + Floor floor = subLocation.floors[i]; + list.add( + ListTile( + title: Text( + "${floor.name}", + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: LOCATION_FONT_SIZE), + ), + subtitle: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + (100 * percentAvailability(floor)).toInt().toString() + + '% Availability', + // style: TextStyle(color: Colors.black), + )), + Align( + alignment: Alignment.centerLeft, + child: SizedBox( + height: PROGRESS_BAR_HEIGHT, + width: PROGRESS_BAR_WIDTH, + child: ClipRRect( + borderRadius: BorderRadius.circular(BORDER_RADIUS), + child: LinearProgressIndicator( + value: percentAvailability(floor) as double?, + backgroundColor: Colors.grey[BACKGROUND_GREY_SHADE], + valueColor: AlwaysStoppedAnimation( + setIndicatorColor( + percentAvailability(floor), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + return ListView( + physics: BouncingScrollPhysics(), + children: list, + ); + } + + // Calculate the percent available + num percentAvailability(Floor subLocationFloor) => + 1 - subLocationFloor.percentage!; + + // Color options + setIndicatorColor(num percentage) { + if (percentage >= .75) + return Colors.green; + else if (percentage >= .25) + return Colors.yellow; + else + return Colors.red; + } +} diff --git a/lib/ui/availability/availability_display.dart b/lib/ui/availability/availability_display.dart index 52c7dd0ed..dae96567e 100644 --- a/lib/ui/availability/availability_display.dart +++ b/lib/ui/availability/availability_display.dart @@ -1,4 +1,6 @@ import 'package:campus_mobile_experimental/core/models/availability.dart'; +import 'package:campus_mobile_experimental/ui/availability/availability_constants.dart'; +import 'package:campus_mobile_experimental/app_constants.dart'; import 'package:flutter/material.dart'; class AvailabilityDisplay extends StatelessWidget { @@ -7,6 +9,7 @@ class AvailabilityDisplay extends StatelessWidget { required this.model, }) : super(key: key); + /// Models final AvailabilityModel model; @override @@ -21,44 +24,46 @@ class AvailabilityDisplay extends StatelessWidget { Widget buildLocationTitle() { return Container( - margin: EdgeInsets.symmetric(horizontal: 18), - child: ListTile( - title: Text( - model.locationName!, - style: TextStyle(fontSize: 17), - ), - contentPadding: EdgeInsets.all(0), - subtitle: Row( - children: [ - Text( - model.isOpen! ? "Open" : "Closed", - ), - Container( - width: 12, - height: 12, - margin: EdgeInsets.all(5), - decoration: BoxDecoration( - color: model.isOpen! ? Colors.green : Colors.red, - shape: BoxShape.circle, - ), - ), - ], - )), + alignment: Alignment.centerLeft, + margin: EdgeInsets.only( + left: TITLE_SIDE_PADDINGS, + right: TITLE_SIDE_PADDINGS, + bottom: TITLE_BOTTOM_PADDING, + ), + child: Text( + model.name!, + style: TextStyle( + fontSize: LOCATION_FONT_SIZE, + fontWeight: FontWeight.bold, + ), + ), ); } Widget buildAvailabilityBars(BuildContext context) { List locations = []; - + // add any children the model contains to the listview if (model.subLocations!.isNotEmpty) { - for (AvailabilityModel subLocation in model.subLocations!) { + for (SubLocations subLocation in model.subLocations!) { locations.add( ListTile( - title: Text(subLocation.locationName!, - style: TextStyle( - fontSize: 17, - )), - subtitle: Column(children: [ + onTap: () => subLocation.floors!.length > 0 + ? Navigator.pushNamed( + context, RoutePaths.AvailabilityDetailedView, + arguments: subLocation) + : print('_handleIconClick: no subLocations'), + visualDensity: VisualDensity.compact, + trailing: subLocation.floors!.length > 0 + ? Icon(Icons.arrow_forward_ios_rounded) + : null, + title: Text( + subLocation.name!, + style: TextStyle( + fontSize: LOCATION_FONT_SIZE, + ), + ), + subtitle: Column( + children: [ Align( alignment: Alignment.centerLeft, child: Text( @@ -71,46 +76,41 @@ class AvailabilityDisplay extends StatelessWidget { Align( alignment: Alignment.centerLeft, child: SizedBox( - height: 12, - width: 325, - child: ClipRRect( - borderRadius: BorderRadius.circular(15), - child: LinearProgressIndicator( - value: percentAvailability(subLocation) as double?, - backgroundColor: Colors.grey[200], - valueColor: AlwaysStoppedAnimation( - setIndicatorColor( - percentAvailability(subLocation))), - ))), - ) - ])), + height: PROGRESS_BAR_HEIGHT, + width: PROGRESS_BAR_WIDTH, + child: ClipRRect( + borderRadius: BorderRadius.circular(BORDER_RADIUS), + child: LinearProgressIndicator( + value: percentAvailability(subLocation) as double?, + backgroundColor: Colors.grey[BACKGROUND_GREY_SHADE], + valueColor: AlwaysStoppedAnimation( + setIndicatorColor( + percentAvailability(subLocation), + ), + ), + ), + ), + ), + ), + ], + ), + ), ); } - } else { - locations.add(ListTile( - // title: Text(model.locationName), - title: Column(children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - (100 * percentAvailability(model)).toInt().toString() + - '% Availability', - //style: TextStyle(color: Colors.black), - )), - Align( - alignment: Alignment.centerLeft, - child: SizedBox( - height: 12, - width: 325, - child: ClipRRect( - borderRadius: BorderRadius.circular(15), - child: LinearProgressIndicator( - value: percentAvailability(model) as double?, - backgroundColor: Colors.grey[200], - valueColor: AlwaysStoppedAnimation( - setIndicatorColor(percentAvailability(model)), - ))))) - ]))); + } + + // if no children, return an error container + else { + return Container( + alignment: Alignment.center, + child: Text( + "Data Unavailable", + style: TextStyle(fontSize: LOCATION_FONT_SIZE), + ), + padding: EdgeInsets.only( + top: DATA_UNAVAILABLE_TOP_PADDING, + ), + ); } locations = ListTile.divideTiles(tiles: locations, context: context).toList(); @@ -124,15 +124,7 @@ class AvailabilityDisplay extends StatelessWidget { ); } - num percentAvailability(AvailabilityModel location) { - num percentAvailable = 0.0; - - if (location.isOpen!) { - percentAvailable = 1 - location.percent!; - } - - return percentAvailable; - } + num percentAvailability(SubLocations location) => 1 - location.percentage!; setIndicatorColor(num percentage) { if (percentage >= .75) diff --git a/lib/ui/availability/manage_availability_view.dart b/lib/ui/availability/manage_availability_view.dart index 524bcf355..7f2e4acd2 100644 --- a/lib/ui/availability/manage_availability_view.dart +++ b/lib/ui/availability/manage_availability_view.dart @@ -39,7 +39,7 @@ class _ManageAvailabilityViewState extends State { newOrder.insert(newIndex, item); List orderedLocationNames = []; for (AvailabilityModel? item in newOrder) { - orderedLocationNames.add(item!.locationName); + orderedLocationNames.add(item!.name); } _availabilityDataProvider.reorderLocations(orderedLocationNames); } @@ -47,22 +47,22 @@ class _ManageAvailabilityViewState extends State { List createList(BuildContext context) { List list = []; for (AvailabilityModel? model - in _availabilityDataProvider.availabilityModels) { + in _availabilityDataProvider.availabilityModels) { if (model != null) { list.add(ListTile( - key: Key(model.locationId.toString()), + key: Key(model.name.toString()), title: Text( - model.locationName!, + model.name!, ), leading: Icon( Icons.reorder, ), trailing: Switch( value: Provider.of(context) - .locationViewState[model.locationName]!, + .locationViewState[model.name]!, activeColor: Theme.of(context).buttonColor, onChanged: (_) { - _availabilityDataProvider.toggleLocation(model.locationName); + _availabilityDataProvider.toggleLocation(model.name); }, ), )); @@ -70,4 +70,4 @@ class _ManageAvailabilityViewState extends State { } return list; } -} +} \ No newline at end of file