From 53ff39c5aa39506dc888f04ccbfc2ecd0814b261 Mon Sep 17 00:00:00 2001 From: jayesh Date: Thu, 16 May 2024 18:38:42 +0530 Subject: [PATCH] added stock details screen and api integration --- assets/images/svg/option_chain_icon.svg | 3 + lib/Utils/api_urls.dart | 3 + lib/Utils/text.dart | 11 + lib/Utils/utils.dart | 18 + .../stock_details_model.dart | 335 +++++++ lib/resources/routes/route_name.dart | 3 + lib/resources/routes/routes.dart | 7 +- lib/view/MainScreen/ShortTrade.dart | 886 +++++++++--------- .../stockDetails/stock_details_screen.dart | 383 ++++++++ .../StockDetailsApi/stock_details_api.dart | 28 + pubspec.lock | 10 +- pubspec.yaml | 1 + 12 files changed, 1265 insertions(+), 423 deletions(-) create mode 100644 assets/images/svg/option_chain_icon.svg create mode 100644 lib/model/StockDetailsModel/stock_details_model.dart create mode 100644 lib/view/MainScreen/stockDetails/stock_details_screen.dart create mode 100644 lib/view_model/StockDetailsApi/stock_details_api.dart diff --git a/assets/images/svg/option_chain_icon.svg b/assets/images/svg/option_chain_icon.svg new file mode 100644 index 0000000..4cfb8a0 --- /dev/null +++ b/assets/images/svg/option_chain_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/Utils/api_urls.dart b/lib/Utils/api_urls.dart index 4a2f927..dcb3b5f 100644 --- a/lib/Utils/api_urls.dart +++ b/lib/Utils/api_urls.dart @@ -82,4 +82,7 @@ class ApiUrls { //kyc static String getKyc = "${base}get-kyc-images"; + + //Stock Details + static String stockDetailsApi = "${base}get-details-of-stock"; } diff --git a/lib/Utils/text.dart b/lib/Utils/text.dart index 93479f2..8eea1fc 100644 --- a/lib/Utils/text.dart +++ b/lib/Utils/text.dart @@ -208,6 +208,17 @@ Widget text12W500(String text) { ); } +Widget text12W600(String text) { + return Text( + text, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + fontFamily: 'hiragino'), + ); +} + Widget text8W400(String text) { return Text( text, diff --git a/lib/Utils/utils.dart b/lib/Utils/utils.dart index 1ca7bad..8e4fbdd 100644 --- a/lib/Utils/utils.dart +++ b/lib/Utils/utils.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; import 'package:permission_handler/permission_handler.dart'; +import 'package:traderscircuit/model/StockDetailsModel/stock_details_model.dart'; class Utils { static Future networkImageToMultipartFile( @@ -38,6 +39,23 @@ class Utils { return file; } + static String removeDecimal(String price) { + final List splittedPrice = price.split("."); + if (splittedPrice[1] == "0") { + return splittedPrice[0]; + } else { + return "${splittedPrice[0]}.${splittedPrice[1]}"; + } + } + + static List extractPriceFromGraph(List data) { + final List priceData = []; + for (final element in data) { + priceData.add(element.close!); + } + return priceData; + } + static showToast(String? msg) { if (msg != null && msg != "null" && msg.isNotEmpty) { Fluttertoast.showToast( diff --git a/lib/model/StockDetailsModel/stock_details_model.dart b/lib/model/StockDetailsModel/stock_details_model.dart new file mode 100644 index 0000000..b5d2368 --- /dev/null +++ b/lib/model/StockDetailsModel/stock_details_model.dart @@ -0,0 +1,335 @@ +class StockDetailsModel { + String? status; + int? statusCode; + String? message; + Data? data; + + StockDetailsModel({this.status, this.statusCode, this.message, this.data}); + + StockDetailsModel.fromJson(Map json) { + status = json['status']; + statusCode = json['status_code']; + message = json['message']; + data = json['data'] != null ? Data.fromJson(json['data']) : null; + } + + Map toJson() { + final Map data = {}; + data['status'] = status; + data['status_code'] = statusCode; + data['message'] = message; + if (this.data != null) { + data['data'] = this.data!.toJson(); + } + return data; + } +} + +class Data { + StockInfo? stockData; + OptionChain? optionChain; + CandleStick? candleStick; + + Data({this.stockData, this.optionChain, this.candleStick}); + + Data.fromJson(Map json) { + stockData = json['stock_data'] != null + ? StockInfo.fromJson(json['stock_data']) + : null; + optionChain = json['option_chain'] != null + ? OptionChain.fromJson(json['option_chain']) + : null; + candleStick = json['candle_stick'] != null + ? CandleStick.fromJson(json['candle_stick']) + : null; + } + + Map toJson() { + final Map data = {}; + if (stockData != null) { + data['stock_data'] = stockData!.toJson(); + } + if (optionChain != null) { + data['option_chain'] = optionChain!.toJson(); + } + if (candleStick != null) { + data['candle_stick'] = candleStick!.toJson(); + } + return data; + } +} + +class StockInfo { + Ohlc? ohlc; + String? timestamp; + String? instrumentToken; + String? symbol; + double? lastPrice; + int? volume; + double? averagePrice; + int? oi; + double? netChange; + int? totalBuyQuantity; + int? totalSellQuantity; + double? lowerCircuitLimit; + double? upperCircuitLimit; + String? lastTradeTime; + int? oiDayHigh; + int? oiDayLow; + + StockInfo( + {this.ohlc, + this.timestamp, + this.instrumentToken, + this.symbol, + this.lastPrice, + this.volume, + this.averagePrice, + this.oi, + this.netChange, + this.totalBuyQuantity, + this.totalSellQuantity, + this.lowerCircuitLimit, + this.upperCircuitLimit, + this.lastTradeTime, + this.oiDayHigh, + this.oiDayLow}); + + StockInfo.fromJson(Map json) { + ohlc = json['ohlc'] != null ? Ohlc.fromJson(json['ohlc']) : null; + timestamp = json['timestamp']; + instrumentToken = json['instrument_token']; + symbol = json['symbol']; + lastPrice = json['last_price']; + volume = json['volume']; + averagePrice = json['average_price']; + oi = json['oi']; + netChange = json['net_change']; + totalBuyQuantity = json['total_buy_quantity']; + totalSellQuantity = json['total_sell_quantity']; + lowerCircuitLimit = json['lower_circuit_limit']; + upperCircuitLimit = json['upper_circuit_limit']; + lastTradeTime = json['last_trade_time']; + oiDayHigh = json['oi_day_high']; + oiDayLow = json['oi_day_low']; + } + + Map toJson() { + final Map data = {}; + if (ohlc != null) { + data['ohlc'] = ohlc!.toJson(); + } + data['timestamp'] = timestamp; + data['instrument_token'] = instrumentToken; + data['symbol'] = symbol; + data['last_price'] = lastPrice; + data['volume'] = volume; + data['average_price'] = averagePrice; + data['oi'] = oi; + data['net_change'] = netChange; + data['total_buy_quantity'] = totalBuyQuantity; + data['total_sell_quantity'] = totalSellQuantity; + data['lower_circuit_limit'] = lowerCircuitLimit; + data['upper_circuit_limit'] = upperCircuitLimit; + data['last_trade_time'] = lastTradeTime; + data['oi_day_high'] = oiDayHigh; + data['oi_day_low'] = oiDayLow; + return data; + } +} + +class Ohlc { + double? open; + double? high; + double? low; + double? close; + + Ohlc({this.open, this.high, this.low, this.close}); + + Ohlc.fromJson(Map json) { + open = json['open'].toDouble(); + high = json['high'].toDouble(); + low = json['low']; + close = json['close'].toDouble(); + } + + Map toJson() { + final Map data = {}; + data['open'] = open; + data['high'] = high; + data['low'] = low; + data['close'] = close; + return data; + } +} + +class OptionChain { + String? status; + List? data2; + + OptionChain({this.status, this.data2}); + + OptionChain.fromJson(Map json) { + status = json['status']; + if (json['data2'] != null) { + data2 = []; + json['data2'].forEach((v) { + data2!.add(Data2.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['status'] = status; + if (data2 != null) { + data['data2'] = data2!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Data2 { + String? name; + String? segment; + String? exchange; + String? expiry; + bool? weekly; + String? instrumentKey; + String? exchangeToken; + String? tradingSymbol; + int? tickSize; + int? lotSize; + String? instrumentType; + int? freezeQuantity; + String? underlyingKey; + String? underlyingType; + String? underlyingSymbol; + int? strikePrice; + int? minimumLot; + + Data2( + {this.name, + this.segment, + this.exchange, + this.expiry, + this.weekly, + this.instrumentKey, + this.exchangeToken, + this.tradingSymbol, + this.tickSize, + this.lotSize, + this.instrumentType, + this.freezeQuantity, + this.underlyingKey, + this.underlyingType, + this.underlyingSymbol, + this.strikePrice, + this.minimumLot}); + + Data2.fromJson(Map json) { + name = json['name']; + segment = json['segment']; + exchange = json['exchange']; + expiry = json['expiry']; + weekly = json['weekly']; + instrumentKey = json['instrument_key']; + exchangeToken = json['exchange_token']; + tradingSymbol = json['trading_symbol']; + tickSize = json['tick_size']; + lotSize = json['lot_size']; + instrumentType = json['instrument_type']; + freezeQuantity = json['freeze_quantity']; + underlyingKey = json['underlying_key']; + underlyingType = json['underlying_type']; + underlyingSymbol = json['underlying_symbol']; + strikePrice = json['strike_price']; + minimumLot = json['minimum_lot']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['segment'] = segment; + data['exchange'] = exchange; + data['expiry'] = expiry; + data['weekly'] = weekly; + data['instrument_key'] = instrumentKey; + data['exchange_token'] = exchangeToken; + data['trading_symbol'] = tradingSymbol; + data['tick_size'] = tickSize; + data['lot_size'] = lotSize; + data['instrument_type'] = instrumentType; + data['freeze_quantity'] = freezeQuantity; + data['underlying_key'] = underlyingKey; + data['underlying_type'] = underlyingType; + data['underlying_symbol'] = underlyingSymbol; + data['strike_price'] = strikePrice; + data['minimum_lot'] = minimumLot; + return data; + } +} + +class CandleStick { + List? candles; + + CandleStick({this.candles}); + + CandleStick.fromJson(Map json) { + if (json['candles'] != null) { + candles = []; + json['candles'].forEach((v) { + candles!.add(Candles.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (candles != null) { + data['candles'] = candles!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Candles { + String? timestamp; + double? open; + double? high; + double? low; + double? close; + int? volume; + int? openInterest; + + Candles( + {this.timestamp, + this.open, + this.high, + this.low, + this.close, + this.volume, + this.openInterest}); + + Candles.fromJson(Map json) { + timestamp = json['timestamp']; + open = json['open']; + high = json['high']; + low = json['low']; + close = json['close']; + volume = json['volume']; + openInterest = json['open_interest']; + } + + Map toJson() { + final Map data = {}; + data['timestamp'] = timestamp; + data['open'] = open; + data['high'] = high; + data['low'] = low; + data['close'] = close; + data['volume'] = volume; + data['open_interest'] = openInterest; + return data; + } +} diff --git a/lib/resources/routes/route_name.dart b/lib/resources/routes/route_name.dart index 24b578d..2653a3f 100644 --- a/lib/resources/routes/route_name.dart +++ b/lib/resources/routes/route_name.dart @@ -55,4 +55,7 @@ class RouteName { static const String videosmore = '/videosmore'; static const String audiomore = '/audiomore'; static const String bookmore = '/bookmore'; + + //stock deatils + static const String stockDetails = '/stockDetails'; } diff --git a/lib/resources/routes/routes.dart b/lib/resources/routes/routes.dart index 283d539..91a9b1e 100644 --- a/lib/resources/routes/routes.dart +++ b/lib/resources/routes/routes.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'package:traderscircuit/Utils/Common/noInternet.dart'; import 'package:traderscircuit/resources/routes/route_name.dart'; import 'package:traderscircuit/view/MainScreen/ExploreUnseen.dart'; +import 'package:traderscircuit/view/MainScreen/stockDetails/stock_details_screen.dart'; import 'package:traderscircuit/view/Sidemenu/AboutUs.dart'; import 'package:traderscircuit/view/Sidemenu/ContentByte/ContentBytes.dart'; import 'package:traderscircuit/view/Sidemenu/ContentByte/AudioMore.dart'; @@ -86,7 +87,7 @@ class AppRoutes { ), GetPage( name: RouteName.verifyotp, - page: () => VerifyOTP(), + page: () => VerifyOTP(), ), //secureaccess @@ -190,6 +191,10 @@ class AppRoutes { GetPage( name: RouteName.myProfileScreen, page: () => const MyProfileScreen(), + ), //stock details + GetPage( + name: RouteName.stockDetails, + page: () => const StockDetailsScreen(), ) ]; } diff --git a/lib/view/MainScreen/ShortTrade.dart b/lib/view/MainScreen/ShortTrade.dart index 74a6c49..0344a08 100644 --- a/lib/view/MainScreen/ShortTrade.dart +++ b/lib/view/MainScreen/ShortTrade.dart @@ -18,6 +18,7 @@ import 'package:traderscircuit/controller/contact_us_controller.dart'; import 'package:traderscircuit/controller/products_controller.dart'; import 'package:traderscircuit/model/ProductsModel/call_recommendations_model.dart'; import 'package:traderscircuit/view/MainScreen/MainScreen.dart'; +import 'package:traderscircuit/view/MainScreen/stockDetails/stock_details_screen.dart'; import 'package:traderscircuit/view/Sidemenu/Sidemenu.dart'; import 'package:traderscircuit/view/onBoarding/splashScreen1.dart'; import 'package:traderscircuit/view_model/ProductsApi/products_api.dart'; @@ -107,7 +108,7 @@ class _ShortTradeState extends State { // // size: 27.sp, // // ), // ), - // ), + // ),j body: Obx( () => Stack( children: [ @@ -259,6 +260,8 @@ class _ShortTradeState extends State { padding: EdgeInsets.only(top: 20.h, bottom: 5.h), child: productsController.selectedIndex.value == 0 ? cardSwingWidget( + instrumentName: callRecommendationsModel + .data!.activeCalls![index].instrumentKey!, pdfLink: "", image: callRecommendationsModel .data!.activeCalls![index].stockImage!, @@ -276,6 +279,8 @@ class _ShortTradeState extends State { ) : productsController.selectedIndex.value == 1 ? cardMultibaggerWidget( + instrumentName: callRecommendationsModel + .data!.activeCalls![index].instrumentKey!, image: callRecommendationsModel .data!.activeCalls![index].stockImage!, action: callRecommendationsModel @@ -297,6 +302,8 @@ class _ShortTradeState extends State { .data!.activeCalls![index].docs!, ) : cardOptionWidget( + instrumentName: callRecommendationsModel + .data!.activeCalls![index].instrumentKey!, pdfLink: "", image: callRecommendationsModel .data!.activeCalls![index].stockImage!, @@ -373,6 +380,8 @@ class _ShortTradeState extends State { padding: EdgeInsets.only(top: 20.h, bottom: 5.h), child: productsController.selectedIndex.value == 0 ? cardSwingWidget( + instrumentName: callRecommendationsModel + .data!.exitedCalls![index].instrumentKey!, pdfLink: callRecommendationsModel .data!.exitedCalls![index].docs!, image: callRecommendationsModel @@ -391,6 +400,8 @@ class _ShortTradeState extends State { ) : productsController.selectedIndex.value == 1 ? cardMultibaggerWidget( + instrumentName: callRecommendationsModel + .data!.exitedCalls![index].instrumentKey!, image: callRecommendationsModel .data!.exitedCalls![index].stockImage!, action: callRecommendationsModel @@ -412,6 +423,8 @@ class _ShortTradeState extends State { .data!.exitedCalls![index].docs!, ) : cardOptionWidget( + instrumentName: callRecommendationsModel + .data!.exitedCalls![index].instrumentKey!, pdfLink: callRecommendationsModel .data!.exitedCalls![index].docs!, image: callRecommendationsModel @@ -448,155 +461,165 @@ class _ShortTradeState extends State { required String action, required String pdfLink, required String image, + required String instrumentName, }) { ContactUsController contactUsController = Get.put(ContactUsController()); - return Container( - padding: const EdgeInsets.all(15), - width: 398, - height: 300, - decoration: ShapeDecoration( - color: const Color(0x232C79ED), - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1, color: Color(0x994A73FB)), - borderRadius: BorderRadius.circular(8), + return InkWell( + onTap: () { + Get.to(const StockDetailsScreen(), arguments: { + "instrument_name": instrumentName, + }); + }, + child: Container( + padding: const EdgeInsets.all(15), + width: 398, + height: 300, + decoration: ShapeDecoration( + color: const Color(0x232C79ED), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x994A73FB)), + borderRadius: BorderRadius.circular(8), + ), ), - ), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CachedNetworkImage( - imageUrl: image, - width: 78.29, - height: 31, - ), - const Gap(10), - text16W700(text), - const Spacer(), - Container( - width: 62.w, - height: 25.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.r), - color: action == "Buy" - ? const Color(0xFF00FF19) - : action == "Exit" - ? const Color(0Xff6C0000) - : const Color(0xFFFFCE00), - ), - child: Center( - child: text14W600_1B1B1B( - action, - clr: action == "Exit" - ? Colors.white - : const Color(0Xff1B1B1B), - )), - ), - const Gap(15), - ], - ), - const Gap(30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(optiontype), - sizedBoxHeight(5.h), - text12W400_979797('Option Type'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(price), - sizedBoxHeight(5.h), - text12W400_979797('Strike Price'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(contactUsController.formatedDateMethod(date)), - sizedBoxHeight(5.h), - text12W400_979797('Expiry Date'), - ], - ), - ], - ), - const Gap(25), - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(price1), - sizedBoxHeight(5.h), - text12W400_979797('Target Price'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(stoploss), - sizedBoxHeight(5.h), - text12W400_979797('Stop Loss'), - ], - ), - ], - ), - const Gap(30), - Container( - width: 398, - height: 52, - decoration: const ShapeDecoration( - color: Color(0x332C79ED), - shape: RoundedRectangleBorder( - side: BorderSide(width: 1, color: Color(0x994A73FB)), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - text15W600(premium), - const Gap(5), - text14W400_979797('/ Premium'), + CachedNetworkImage( + imageUrl: image, + width: 78.29, + height: 31, + ), + const Gap(10), + text16W700(text), + const Spacer(), + Container( + width: 62.w, + height: 25.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.r), + color: action == "Buy" + ? const Color(0xFF00FF19) + : action == "Exit" + ? const Color(0Xff6C0000) + : const Color(0xFFFFCE00), + ), + child: Center( + child: text14W600_1B1B1B( + action, + clr: action == "Exit" + ? Colors.white + : const Color(0Xff1B1B1B), + )), + ), + const Gap(15), ], ), - ) - ], - ), - Positioned( - right: 0, - child: SvgPicture.asset("assets/images/svg/card_decoration.svg")) - ], + const Gap(30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(optiontype), + sizedBoxHeight(5.h), + text12W400_979797('Option Type'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(price), + sizedBoxHeight(5.h), + text12W400_979797('Strike Price'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600( + contactUsController.formatedDateMethod(date)), + sizedBoxHeight(5.h), + text12W400_979797('Expiry Date'), + ], + ), + ], + ), + const Gap(25), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(price1), + sizedBoxHeight(5.h), + text12W400_979797('Target Price'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(stoploss), + sizedBoxHeight(5.h), + text12W400_979797('Stop Loss'), + ], + ), + ], + ), + const Gap(30), + Container( + width: 398, + height: 52, + decoration: const ShapeDecoration( + color: Color(0x332C79ED), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: Color(0x994A73FB)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + text15W600(premium), + const Gap(5), + text14W400_979797('/ Premium'), + ], + ), + ) + ], + ), + Positioned( + right: 0, + child: + SvgPicture.asset("assets/images/svg/card_decoration.svg")) + ], + ), ), ); } @@ -612,164 +635,176 @@ class _ShortTradeState extends State { required String action, required String pdfLink, required String image, + required String instrumentName, }) { ContactUsController contactUsController = Get.put(ContactUsController()); - return Container( - padding: const EdgeInsets.all(15), - width: 398, - height: 300, - decoration: ShapeDecoration( - color: const Color(0x232C79ED), - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1, color: Color(0x994A73FB)), - borderRadius: BorderRadius.circular(8), - ), - ), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CachedNetworkImage( - imageUrl: image, - width: 78.29, - height: 31, - ), - const Gap(10), - text16W700(text), - const Spacer(), - Container( - width: 62.w, - height: 25.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.r), - color: action == "Buy" - ? const Color(0xFF00FF19) - : action == "Exit" - ? const Color(0Xff6C0000) - : const Color(0xFFFFCE00), - ), - child: Center( - child: text14W600_1B1B1B( - action, - clr: action == "Exit" - ? Colors.white - : const Color(0Xff1B1B1B), - )), - ), - const Gap(15), - ], - ), - const Gap(30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(price), - sizedBoxHeight(5.h), - text12W400_979797('Entry Price'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(contactUsController.formatedDateMethod(date)), - sizedBoxHeight(5.h), - text12W400_979797('Date Of Recommendation'), - ], - ), - ], - ), - const Gap(25), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(returns), - sizedBoxHeight(5.h), - text12W400_979797('Target Price'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(stoploss), - sizedBoxHeight(5.h), - text12W400_979797('Stop Loss'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(duration), - sizedBoxHeight(5.h), - text12W400_979797('Duration'), - ], - ), - ], - ), - const Gap(30), - GestureDetector( - onTap: () { - Utils.openFile(url: pdfLink, fileName: "${text}_report.pdf"); - }, - child: Container( - width: 398, - height: 52, - decoration: const ShapeDecoration( - color: Color(0x332C79ED), - shape: RoundedRectangleBorder( - side: BorderSide(width: 1, color: Color(0x994A73FB)), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/png/pdf.png', - height: 20.h, - width: 20.w, - ), - text15W600("Download Pdf"), - ], - ), - ), - ) - ], + return InkWell( + onTap: () { + Get.to(const StockDetailsScreen(), arguments: { + "instrument_name": instrumentName, + }); + }, + child: Container( + padding: const EdgeInsets.all(15), + width: 398, + height: 300, + decoration: ShapeDecoration( + color: const Color(0x232C79ED), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x994A73FB)), + borderRadius: BorderRadius.circular(8), ), - Positioned( - right: 0, - child: SvgPicture.asset("assets/images/svg/card_decoration.svg")) - ], + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: image, + width: 78.29, + height: 31, + ), + const Gap(10), + text16W700(text), + const Spacer(), + Container( + width: 62.w, + height: 25.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.r), + color: action == "Buy" + ? const Color(0xFF00FF19) + : action == "Exit" + ? const Color(0Xff6C0000) + : const Color(0xFFFFCE00), + ), + child: Center( + child: text14W600_1B1B1B( + action, + clr: action == "Exit" + ? Colors.white + : const Color(0Xff1B1B1B), + )), + ), + const Gap(15), + ], + ), + const Gap(30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(price), + sizedBoxHeight(5.h), + text12W400_979797('Entry Price'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600( + contactUsController.formatedDateMethod(date)), + sizedBoxHeight(5.h), + text12W400_979797('Date Of Recommendation'), + ], + ), + ], + ), + const Gap(25), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(returns), + sizedBoxHeight(5.h), + text12W400_979797('Target Price'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(stoploss), + sizedBoxHeight(5.h), + text12W400_979797('Stop Loss'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(duration), + sizedBoxHeight(5.h), + text12W400_979797('Duration'), + ], + ), + ], + ), + const Gap(30), + GestureDetector( + onTap: () { + Utils.openFile( + url: pdfLink, fileName: "${text}_report.pdf"); + }, + child: Container( + width: 398, + height: 52, + decoration: const ShapeDecoration( + color: Color(0x332C79ED), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: Color(0x994A73FB)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/png/pdf.png', + height: 20.h, + width: 20.w, + ), + Gap(8), + text15W600("Download Pdf"), + ], + ), + ), + ) + ], + ), + Positioned( + right: 0, + child: + SvgPicture.asset("assets/images/svg/card_decoration.svg")) + ], + ), ), ); } @@ -783,133 +818,142 @@ class _ShortTradeState extends State { required String time, required String pdfLink, required String action, + required String instrumentName, }) { - return Container( - padding: const EdgeInsets.all(15), - width: 398, - height: 285, - decoration: ShapeDecoration( - color: const Color(0x232C79ED), - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1, color: Color(0x994A73FB)), - borderRadius: BorderRadius.circular(8), + return InkWell( + onTap: () { + Get.to(const StockDetailsScreen(), arguments: { + "instrument_name": instrumentName, + }); + }, + child: Container( + padding: const EdgeInsets.all(15), + width: 398, + height: 285, + decoration: ShapeDecoration( + color: const Color(0x232C79ED), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x994A73FB)), + borderRadius: BorderRadius.circular(8), + ), ), - ), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CachedNetworkImage( - imageUrl: image, - width: 78.29, - height: 31, - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - text16W700(text), - const Spacer(), - Container( - width: 62.w, - height: 25.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.r), - color: action == "Buy" - ? const Color(0xFF00FF19) - : action == "Exit" - ? const Color(0Xff6C0000) - : const Color(0xFFFFCE00), - ), - child: Center( - child: text14W600_1B1B1B( - action, - clr: action == "Exit" - ? Colors.white - : const Color(0Xff1B1B1B), - )), - ), - const Gap(15), - ], - ), - const Gap(30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(amount), - sizedBoxHeight(5.h), - text12W400_979797('Entry Price'), - ], - ), - const Gap(10), - Container( - width: 2, - height: 35, - color: Colors.white.withOpacity(0.30000001192092896), - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(targetamount), - sizedBoxHeight(5.h), - text12W400_979797('Target Price'), - ], - ), - // const Gap(10), - // Container( - // width: 2, - // height: 35, - // color: Colors.white.withOpacity(0.30000001192092896), - // ), - // const Gap(10), - ], - ), - const Gap(10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - text15W600(stoploss), - sizedBoxHeight(5.h), - text12W400_979797('Stop Loss'), - ], - ), - const Gap(30), - Container( - width: 398, - height: 52, - decoration: const ShapeDecoration( - color: Color(0x332C79ED), - shape: RoundedRectangleBorder( - side: BorderSide(width: 1, color: Color(0x994A73FB)), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - text15W600(time), - const Gap(5), - text14W400_979797('/ Time Horizon'), + CachedNetworkImage( + imageUrl: image, + width: 78.29, + height: 31, + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + text16W700(text), + const Spacer(), + Container( + width: 62.w, + height: 25.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.r), + color: action == "Buy" + ? const Color(0xFF00FF19) + : action == "Exit" + ? const Color(0Xff6C0000) + : const Color(0xFFFFCE00), + ), + child: Center( + child: text14W600_1B1B1B( + action, + clr: action == "Exit" + ? Colors.white + : const Color(0Xff1B1B1B), + )), + ), + const Gap(15), ], ), - ) - ], - ), - Positioned( - right: 0, - child: SvgPicture.asset("assets/images/svg/card_decoration.svg")) - ], + const Gap(30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(amount), + sizedBoxHeight(5.h), + text12W400_979797('Entry Price'), + ], + ), + const Gap(10), + Container( + width: 2, + height: 35, + color: Colors.white.withOpacity(0.30000001192092896), + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(targetamount), + sizedBoxHeight(5.h), + text12W400_979797('Target Price'), + ], + ), + // const Gap(10), + // Container( + // width: 2, + // height: 35, + // color: Colors.white.withOpacity(0.30000001192092896), + // ), + // const Gap(10), + ], + ), + const Gap(10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text15W600(stoploss), + sizedBoxHeight(5.h), + text12W400_979797('Stop Loss'), + ], + ), + const Gap(30), + Container( + width: 398, + height: 52, + decoration: const ShapeDecoration( + color: Color(0x332C79ED), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: Color(0x994A73FB)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + text15W600(time), + const Gap(5), + text14W400_979797('/ Time Horizon'), + ], + ), + ) + ], + ), + Positioned( + right: 0, + child: + SvgPicture.asset("assets/images/svg/card_decoration.svg")) + ], + ), ), ); } diff --git a/lib/view/MainScreen/stockDetails/stock_details_screen.dart b/lib/view/MainScreen/stockDetails/stock_details_screen.dart new file mode 100644 index 0000000..4ac4bba --- /dev/null +++ b/lib/view/MainScreen/stockDetails/stock_details_screen.dart @@ -0,0 +1,383 @@ +import 'dart:developer' as dv; + +import 'package:dio/dio.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart' hide FormData; +import 'package:traderscircuit/Utils/text.dart'; +import 'package:traderscircuit/Utils/utils.dart'; +import 'package:traderscircuit/model/StockDetailsModel/stock_details_model.dart'; +import 'package:traderscircuit/view_model/StockDetailsApi/stock_details_api.dart'; + +import 'dart:math'; +import '../../../Utils/Common/CommonAppBar.dart'; +import '../../onBoarding/splashScreen1.dart'; + +class StockDetailsScreen extends StatefulWidget { + const StockDetailsScreen({super.key}); + + @override + State createState() => _StockDetailsScreenState(); +} + +class _StockDetailsScreenState extends State { + RxBool isLoading = true.obs; + StockDetailsModel? stockDetailsModel; + @override + void initState() { + String instrumentName = Get.arguments["instrument_name"]; + dv.log(instrumentName); + StockDetailsApi() + .getStockDetails( + FormData.fromMap({"upstox_instrument_key": instrumentName})) + .then((value) { + final Map data = value.data; + + Map data1 = data["data"]['stock_data']['data']; + String dynamicKey = data1.keys.first; + // Access nested data using dynamic key + Map dynamicData = data1[dynamicKey]; + + List? candles = []; + + for (var a in data["data"]['candle_stick']["data"]["candles"]) { + candles.add(Candles( + timestamp: a[0], + open: a[1].toDouble(), + high: a[2].toDouble(), + low: a[3].toDouble(), + close: a[4].toDouble(), + volume: a[5].toInt(), + openInterest: a[6], + )); + } + + stockDetailsModel = StockDetailsModel( + status: data["status"], + message: data["message"], + statusCode: data["status_code"], + data: Data( + stockData: StockInfo( + ohlc: Ohlc.fromJson(dynamicData['ohlc']), + averagePrice: dynamicData['average_price'], + instrumentToken: dynamicData['instrument_token'], + lastPrice: dynamicData['last_price'].toDouble(), + lastTradeTime: dynamicData['last_trade_time'], + lowerCircuitLimit: dynamicData['lower_circuit_limit'], + netChange: dynamicData['net_change'], + oi: dynamicData['oi'], + oiDayHigh: dynamicData['oi_day_high'], + oiDayLow: dynamicData['oi_day_low'], + symbol: dynamicData['symbol'], + timestamp: dynamicData['timestamp'], + totalBuyQuantity: dynamicData['total_buy_quantity'], + totalSellQuantity: dynamicData['total_sell_quantity'], + upperCircuitLimit: dynamicData['upper_circuit_limit'], + volume: dynamicData['volume'], + ), + candleStick: CandleStick(candles: candles), + optionChain: OptionChain.fromJson(data["data"]['option_chain']))); + + isLoading.value = false; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + drawerEnableOpenDragGesture: false, + extendBody: true, + appBar: const CommonAppbar( + titleTxt: "", + ), + body: Obx(() => Stack(children: [ + const CommonBlurLeft(), + const CommonBlurRight(), + isLoading.value + ? const Center( + child: CircularProgressIndicator( + color: Color(0xFF0093FF), + ), + ) + : Padding( + padding: const EdgeInsets.all(15.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W700( + stockDetailsModel!.data!.stockData!.symbol!), + const Gap(14), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + text25W600( + "₹${stockDetailsModel!.data!.stockData!.averagePrice!.toString()}"), + Container( + width: 145, + height: 40, + decoration: ShapeDecoration( + color: const Color(0xFF0093FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + SvgPicture.asset( + "assets/images/svg/option_chain_icon.svg"), + const Gap(5), + text12W600("Option Chain"), + ], + )), + ], + ), + const Gap(8), + text12W400( + "${stockDetailsModel!.data!.stockData!.netChange} 1D"), + const Gap(55), + Container( + margin: const EdgeInsets.symmetric(horizontal: 15), + width: Get.width, + height: 220, + color: Colors.transparent, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + fitInsideHorizontally: true, + fitInsideVertically: true, + tooltipRoundedRadius: 24, + getTooltipItems: ( + List touchedBarSpots, + ) { + return touchedBarSpots.map((barSpot) { + return LineTooltipItem( + "₹ ${Utils.removeDecimal( + Utils.extractPriceFromGraph( + stockDetailsModel! + .data! + .candleStick! + .candles!)[ + barSpot.x.toInt()] + .toStringAsFixed(2), + ).replaceAllMapped( + RegExp( + r'(\d{1,3})(?=(\d{3})+(?!\d))', + ), + (Match m) => '${m[1]},', + )}", + const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ); + }).toList(); + }, + ), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + drawHorizontalLine: false, + horizontalInterval: 4, + getDrawingHorizontalLine: (value) { + return const FlLine( + color: Color( + 0xff37434d, + ), + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return const FlLine( + color: Color( + 0xff0093FF, + ), + strokeWidth: 1, + ); + }, + ), + titlesData: const FlTitlesData( + show: false, + ), + borderData: FlBorderData( + show: false, + ), + minX: 0, + maxX: (stockDetailsModel! + .data!.candleStick!.candles!.length + .toDouble()) - + 1, + minY: Utils.extractPriceFromGraph( + stockDetailsModel! + .data!.candleStick!.candles!) + .reduce(min) + .toDouble(), + maxY: Utils.extractPriceFromGraph( + stockDetailsModel! + .data!.candleStick!.candles!) + .reduce(max) + .toDouble(), + lineBarsData: [ + LineChartBarData( + color: const Color(0xFF0093FF), + spots: listData( + Utils.extractPriceFromGraph( + stockDetailsModel! + .data!.candleStick!.candles!), + ), + barWidth: 3, + isStrokeCapRound: true, + dotData: const FlDotData( + show: false, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + const Color( + 0xFF0093FF, + ).withOpacity(.01), + const Color( + 0xFF0093FF, + ).withOpacity(.3), + ], + )), + ), + ], + ), + ), + ), + const Gap(20), + text18W400("Overview"), + const Gap(15), + text20W400("Performance"), + const Gap(22), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W400('Today’s Low', + clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel! + .data!.stockData!.ohlc!.low! + .toString(), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + text16W400('Today’s High', + clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel! + .data!.stockData!.ohlc!.high! + .toString(), + ) + ], + ) + ], + ), + Gap(40), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W400('Open price', + clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel! + .data!.stockData!.ohlc!.open + .toString(), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W400('Prev. Close', + clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel! + .data!.candleStick!.candles![0].close + .toString(), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W400('Volume', clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel!.data!.stockData!.volume + .toString(), + ) + ], + ) + ], + ), + Gap(25), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W400('Lower Circuit', + clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel! + .data!.stockData!.lowerCircuitLimit + .toString(), + ) + ], + ), + Gap(25), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text16W400('Upper Circuit', + clr: Color(0xFFF979797)), + Gap(5), + text16W500( + stockDetailsModel! + .data!.stockData!.upperCircuitLimit + .toString(), + ) + ], + ) + ], + ), + Gap(25), + ], + ), + ), + ), + ])), + ); + } +} + +List listData(List data) { + return data.asMap().entries.map((e) { + return FlSpot(e.key.toDouble(), e.value.toDouble()); + }).toList(); +} diff --git a/lib/view_model/StockDetailsApi/stock_details_api.dart b/lib/view_model/StockDetailsApi/stock_details_api.dart new file mode 100644 index 0000000..02cd573 --- /dev/null +++ b/lib/view_model/StockDetailsApi/stock_details_api.dart @@ -0,0 +1,28 @@ +import 'dart:developer'; + +import 'package:dio/dio.dart'; + +import '../../Utils/api_urls.dart'; +import '../../Utils/base_manager.dart'; +import '../../data/network/network_api_services.dart'; + +class StockDetailsApi { + Future> getStockDetails(FormData data) async { + final response = await NetworkApiServices().postApi( + data, + ApiUrls.stockDetailsApi, + ); + //log(response.data.toString()); + if (response.status == ResponseStatus.SUCCESS) { + Map responseData = + Map.from(response.data); + if (responseData['status'] == "success") { + return response; + } else { + return ResponseData( + responseData['message'], ResponseStatus.FAILED); + } + } + return response; + } +} diff --git a/pubspec.lock b/pubspec.lock index 26b4424..c4dd6d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -329,6 +329,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + url: "https://pub.dev" + source: hosted + version: "0.68.0" flutter: dependency: "direct main" description: flutter @@ -1399,5 +1407,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index dfb3666..da0d611 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: timeago: ^3.6.1 device_info_plus: ^8.2.2 permission_handler: ^11.3.1 + fl_chart: ^0.68.0 dev_dependencies: flutter_test: