Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions packages/smooth_app/lib/data_models/users_profile_data.dart
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Irrelevant now as you know well: we should use PriceUser from the latest openfoodfacts-dart package.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// TODO(chetanr25): To be implemented in OpenFoodFacts flutter package in [https://github.com/openfoodfacts/smooth-app/tree/develop/packages/smooth_app/lib/data_models] as [UserProfile] JsonSerializable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This TODO indicates that this model should eventually be moved to the openfoodfacts-dart package. It would be good to create an issue in that repo to track this work and link it here.

class UserProfile {
UserProfile({
required this.userId,
required this.priceCount,
required this.priceTypeProductCount,
required this.priceTypeCategoryCount,
required this.priceKindCommunityCount,
required this.priceKindConsumptionCount,
required this.priceCurrencyCount,
required this.priceInProofOwnedCount,
required this.priceInProofNotOwnedCount,
required this.priceNotOwnedInProofOwnedCount,
required this.proofCount,
required this.proofKindCommunityCount,
required this.proofKindConsumptionCount,
required this.locationCount,
required this.locationTypeOsmCountryCount,
required this.productCount,
});

factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
userId: json['user_id'] as String,
priceCount: json['price_count'] as int,
priceTypeProductCount: json['price_type_product_count'] as int,
priceTypeCategoryCount: json['price_type_category_count'] as int,
priceKindCommunityCount: json['price_kind_community_count'] as int,
priceKindConsumptionCount: json['price_kind_consumption_count'] as int,
priceCurrencyCount: json['price_currency_count'] as int,
priceInProofOwnedCount: json['price_in_proof_owned_count'] as int,
priceInProofNotOwnedCount: json['price_in_proof_not_owned_count'] as int,
priceNotOwnedInProofOwnedCount:
json['price_not_owned_in_proof_owned_count'] as int,
proofCount: json['proof_count'] as int,
proofKindCommunityCount: json['proof_kind_community_count'] as int,
proofKindConsumptionCount: json['proof_kind_consumption_count'] as int,
locationCount: json['location_count'] as int,
locationTypeOsmCountryCount:
json['location_type_osm_country_count'] as int,
productCount: json['product_count'] as int,
);
}
final String userId;
final int priceCount;
final int priceTypeProductCount;
final int priceTypeCategoryCount;
final int priceKindCommunityCount;
final int priceKindConsumptionCount;
final int priceCurrencyCount;
final int priceInProofOwnedCount;
final int priceInProofNotOwnedCount;
final int priceNotOwnedInProofOwnedCount;
final int proofCount;
final int proofKindCommunityCount;
final int proofKindConsumptionCount;
final int locationCount;
final int locationTypeOsmCountryCount;
final int productCount;

Map<String, dynamic> toJson() {
return <String, dynamic>{
'user_id': userId,
'price_count': priceCount,
'price_type_product_count': priceTypeProductCount,
'price_type_category_count': priceTypeCategoryCount,
'price_kind_community_count': priceKindCommunityCount,
'price_kind_consumption_count': priceKindConsumptionCount,
'price_currency_count': priceCurrencyCount,
'price_in_proof_owned_count': priceInProofOwnedCount,
'price_in_proof_not_owned_count': priceInProofNotOwnedCount,
'price_not_owned_in_proof_owned_count': priceNotOwnedInProofOwnedCount,
'proof_count': proofCount,
'proof_kind_community_count': proofKindCommunityCount,
'proof_kind_consumption_count': proofKindConsumptionCount,
'location_count': locationCount,
'location_type_osm_country_count': locationTypeOsmCountryCount,
'product_count': productCount,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:smooth_app/pages/preferences/user_preferences_page.dart';
import 'package:smooth_app/pages/prices/get_prices_model.dart';
import 'package:smooth_app/pages/prices/price_button.dart';
import 'package:smooth_app/pages/prices/price_user_button.dart';
import 'package:smooth_app/pages/prices/prices_dashboard.dart';
import 'package:smooth_app/pages/prices/prices_locations_page.dart';
import 'package:smooth_app/pages/prices/prices_page.dart';
import 'package:smooth_app/pages/prices/prices_products_page.dart';
Expand Down Expand Up @@ -46,6 +47,15 @@ class UserPreferencesPrices extends AbstractUserPreferences {
final String userId = ProductQuery.getWriteUser().userId;
final bool isConnected = OpenFoodAPIConfiguration.globalUser != null;
return <UserPreferencesItem>[
if (isConnected)
_getListTile(
'My Dashboard',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be localized in app_en.arb.

() => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) => PricesDashboard(),
),
),
Icons.dashboard),
if (isConnected)
_getListTile(
PriceUserButton.showUserTitle(
Expand Down
78 changes: 78 additions & 0 deletions packages/smooth_app/lib/pages/prices/prices_dashboard.dart
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename as PricesDashboardPage (and the file too).

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/data_models/users_profile_data.dart';
import 'package:smooth_app/helpers/launch_url_helper.dart';
import 'package:smooth_app/pages/prices/prices_dashboard_widget.dart';
import 'package:smooth_app/pages/prices/prices_user_profile.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_app_bar.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';

class PricesDashboard extends StatelessWidget {
PricesDashboard({super.key});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PricesDashboard({super.key});
PricesDashboard();


late final Future<MaybeError<UserProfile>> _userProfile = _fetchUserProfile();

@override
Widget build(BuildContext context) {
return SmoothScaffold(
appBar: SmoothAppBar(
title: const Text('My Dashboard'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localize

actions: <Widget>[
IconButton(
tooltip: 'Open Prices Dashboard in browser',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localize

icon: const Icon(Icons.open_in_new),
onPressed: () async => LaunchUrlHelper.launchURL(
OpenPricesAPIClient.getUri(
path: 'dashboard',
uriHelper: ProductQuery.uriPricesHelper,
).toString(),
),
),
],
),
body: FutureBuilder<MaybeError<UserProfile>>(
future: _userProfile,
builder: (BuildContext context,
AsyncSnapshot<MaybeError<UserProfile>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text(snapshot.error!.toString());
}
final UserProfile userProfile = snapshot.data!.value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check snapshot.connectionState != ConnectionState.done

return Column(
children: <Widget>[
PricesUserProfile(profile: userProfile),
Expanded(
child: PricesDashboardWidget(userProfile: userProfile)),
],
);
}),
);
}

// TODO(chetanr25): To be implemented in OpenFoodFacts flutter package

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This TODO indicates that this function should eventually be moved to the openfoodfacts-dart package. It would be good to create an issue in that repo to track this work and link it here.

static Future<MaybeError<UserProfile>> _fetchUserProfile() async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is now implemented in openfoodfacts-dart.

final String? userId = OpenFoodAPIConfiguration.globalUser?.userId;
final Uri uri = OpenPricesAPIClient.getUri(
path: '/api/v1/users/$userId',
);

final http.Response response =
await HttpHelper().doGetRequest(uri, uriHelper: uriHelperFoodProd);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not PROD, use ProductQuery.uriPricesHelper.

try {
if (response.statusCode == 200) {
final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response);
return MaybeError<UserProfile>.value(
UserProfile.fromJson(decodedResponse),
);
}
} catch (e) {
//
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block is currently empty. At a minimum, log the error for debugging purposes. Ideally, display a user-friendly error message.

    } catch (e) {
      debugPrint('Error fetching user profile: $e'); // Log the error
      // Optionally, display an error message to the user
    }

return MaybeError<UserProfile>.responseError(response);
}
}
201 changes: 201 additions & 0 deletions packages/smooth_app/lib/pages/prices/prices_dashboard_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';

import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/data_models/users_profile_data.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/pages/prices/get_prices_model.dart';
import 'package:smooth_app/pages/prices/price_user_button.dart';
import 'package:smooth_app/pages/prices/prices_proofs_page.dart';
import 'package:smooth_app/pages/prices/product_prices_list.dart';
import 'package:smooth_app/query/product_query.dart';

class PricesDashboardWidget extends StatefulWidget {
const PricesDashboardWidget({super.key, required this.userProfile});
final UserProfile? userProfile;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final UserProfile? userProfile;
final UserProfile userProfile;

@override
State<PricesDashboardWidget> createState() => _PricesDashboardWidgetState();
}

class _PricesDashboardWidgetState extends State<PricesDashboardWidget> {
int selectedIndex = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be faster to make it a String directly.

late Future<MaybeError<GetPricesResult?>> pricesFuture = _getUserPrices();

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);

return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: MEDIUM_SPACE,
children: <Widget>[
categorySwitch(),
const SizedBox(height: SMALL_SPACE),
priceProofButton(widget.userProfile!, appLocalizations),
Copy link

Copilot AI May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Column widget does not have a 'spacing' property. Replace it with SizedBox widgets or use a widget that supports spacing.

Suggested change
spacing: MEDIUM_SPACE,
children: <Widget>[
categorySwitch(),
const SizedBox(height: SMALL_SPACE),
priceProofButton(widget.userProfile!, appLocalizations),
children: <Widget>[
categorySwitch(),
const SizedBox(height: MEDIUM_SPACE),
priceProofButton(widget.userProfile!, appLocalizations),
const SizedBox(height: MEDIUM_SPACE),

Copilot uses AI. Check for mistakes.
FutureBuilder<MaybeError<GetPricesResult?>>(
future: _getUserPrices(),
Copy link

Copilot AI May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FutureBuilder is instantiating a new future every build call rather than using the stored 'pricesFuture'. Replace it with 'pricesFuture' to avoid unnecessary re-fetching.

Suggested change
future: _getUserPrices(),
future: pricesFuture,

Copilot uses AI. Check for mistakes.
builder: (BuildContext context,
AsyncSnapshot<MaybeError<GetPricesResult?>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}

return Expanded(
child: ProductPricesList(
GetPricesModel(
title: appLocalizations.prices_generic_title,
parameters: GetPricesParameters(),
uri: OpenPricesAPIClient.getUri(
path: 'users/${widget.userProfile!.userId}',
uriHelper: ProductQuery.uriPricesHelper,
),
),
pricesResult: snapshot.data!.value,
),
);
},
),
],
);
}

Future<MaybeError<GetPricesResult?>> _getUserPrices() async {
final MaybeError<GetPricesResult?> prices =
await OpenPricesAPIClient.getPrices(
GetPricesParameters()
..owner = OpenFoodAPIConfiguration.globalUser?.userId
..kind = selectedIndex == 0
? ContributionKind.consumption
: ContributionKind.community,
uriHelper: ProductQuery.uriPricesHelper,
);
return prices;
}

/// Toggle between "My Consumption" and "Other Contributions"
Widget categorySwitch() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Widget categorySwitch() {
Widget _categorySwitch() {

return Padding(
padding: const EdgeInsets.all(VERY_LARGE_SPACE),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
flex: 1,
child: customToggleButton(
0,
Icons.shopping_cart,
'Receipts & GDPR requests',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localize.

() => setState(() {
selectedIndex = 0;
pricesFuture = _getUserPrices();
}))),
Expanded(
flex: 1,
child: customToggleButton(1, Icons.people, 'Price labels', () {
setState(() {
selectedIndex = 1;
pricesFuture = _getUserPrices();
});
}),
),
],
),
);
}

Widget customToggleButton(
int index, IconData icon, String label, VoidCallback onTap) {
final Color selectedColor = Theme.of(context).colorScheme.onSurface;
final Color unselectedColor = selectedColor.withAlpha(128);
final bool isSelected = selectedIndex == index;
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: VERY_SMALL_SPACE,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(icon, color: isSelected ? selectedColor : unselectedColor),
const SizedBox(width: VERY_SMALL_SPACE),
Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected ? selectedColor : unselectedColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
if (isSelected)
Container(
alignment: Alignment.center,
width: double.infinity,
height: VERY_SMALL_SPACE,
color: selectedColor,
)
else
const SizedBox(height: VERY_SMALL_SPACE),
],
),
);
}

Widget priceProofButton(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Widget priceProofButton(
Widget _priceProofButton(

UserProfile profile, AppLocalizations appLocalizations) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 1,
child: SmoothCard(
child: ListTile(
onTap: () {
PriceUserButton.showUserPrices(
user: profile.userId,
context: context,
);
},
subtitle: Text(appLocalizations.prices_generic_title),
title: Text(selectedIndex == 0
? profile.priceKindConsumptionCount.toString()
: profile.priceKindCommunityCount.toString()),
trailing: const Icon(Icons.arrow_forward),
),
),
),
Expanded(
flex: 1,
child: SmoothCard(
child: ListTile(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) =>
const PricesProofsPage(selectProof: false),
),
);
},
subtitle: Text(appLocalizations.prices_proof_subtitle),
title: Text(selectedIndex == 0
? profile.proofKindConsumptionCount.toString()
: profile.proofKindCommunityCount.toString()),
trailing: const Icon(Icons.arrow_forward),
),
),
),
],
);
}
}
Loading
Loading