added faiz_dev_tools_imp

faiz_dev_tools_imp
faizatflutter 5 days ago
parent 818a1f8205
commit 11c2ef71c3

@ -26,7 +26,7 @@ var userNotificationCenterDelegate:UNUserNotificationCenterDelegate? = nil
func initializePlatformChannels(){ func initializePlatformChannels(){
if let mainViewController = window.rootViewController as? MainFlutterVC{ // platform initialization suppose to be in foreground if let mainViewController = window?.rootViewController as? MainFlutterVC{ // platform initialization suppose to be in foreground
flutterViewController = mainViewController flutterViewController = mainViewController
HMGPlatformBridge.initialize(flutterViewController: flutterViewController) HMGPlatformBridge.initialize(flutterViewController: flutterViewController)
OpenTokPlatformBridge.initialize(flutterViewController: flutterViewController, registrar: self.registrar(forPlugin: "open-tok")) OpenTokPlatformBridge.initialize(flutterViewController: flutterViewController, registrar: self.registrar(forPlugin: "open-tok"))

@ -5,6 +5,14 @@ import 'package:hmg_patient_app/models/Request.dart';
import 'package:hmg_patient_app/uitl/app_shared_preferences.dart'; import 'package:hmg_patient_app/uitl/app_shared_preferences.dart';
var MAX_SMALL_SCREEN = 660; var MAX_SMALL_SCREEN = 660;
/// Dev Tools
/// Set to [true] API logs are saved, drawer shows "API Logs" & "Environment
/// Config", environment switching is active.
/// Set to [false] All of the above is completely disabled with zero overhead.
const bool ENABLE_DEV_TOOLS = false;
//
final OPENTOK_API_KEY = '46209962'; final OPENTOK_API_KEY = '46209962';
// final OPENTOK_API_KEY = '47464241'; // final OPENTOK_API_KEY = '47464241';
@ -357,7 +365,7 @@ var CAN_PAY_FOR_FOR_WALKIN_APPOINTMENT = 'Services/Doctors.svc/REST/CanPayForWal
var CHANNEL = 3; var CHANNEL = 3;
var GENERAL_ID = 'Cs2020@2016\$2958'; var GENERAL_ID = 'Cs2020@2016\$2958';
var IP_ADDRESS = '10.20.10.20'; var IP_ADDRESS = '10.20.10.20';
var VERSION_ID = 18.7; var VERSION_ID = 21.9;
var SETUP_ID = '91877'; var SETUP_ID = '91877';
var LANGUAGE = 2; var LANGUAGE = 2;
// var PATIENT_OUT_SA = 0; // var PATIENT_OUT_SA = 0;
@ -580,7 +588,8 @@ var GET_CUSTOMER_INFO = "VerifyCustomer";
//Pharmacy //Pharmacy
var GET_PHARMACY_CATEGORISE = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0'; var GET_PHARMACY_CATEGORISE =
'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0';
var GET_OFFERS_CATEGORISE = 'discountcategories'; var GET_OFFERS_CATEGORISE = 'discountcategories';
var GET_OFFERS_PRODUCTS = 'offerproducts/'; var GET_OFFERS_PRODUCTS = 'offerproducts/';
var GET_CATEGORISE_PARENT = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id='; var GET_CATEGORISE_PARENT = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=';

@ -8,6 +8,7 @@ import 'package:hmg_patient_app/config/shared_pref_kay.dart';
import 'package:hmg_patient_app/core/service/medical/vital_sign_service.dart'; import 'package:hmg_patient_app/core/service/medical/vital_sign_service.dart';
import 'package:hmg_patient_app/core/service/packages_offers/PackagesOffersServices.dart'; import 'package:hmg_patient_app/core/service/packages_offers/PackagesOffersServices.dart';
import 'package:hmg_patient_app/core/viewModels/project_view_model.dart'; import 'package:hmg_patient_app/core/viewModels/project_view_model.dart';
import 'package:hmg_patient_app/dev_tools/api_logger_service.dart';
import 'package:hmg_patient_app/models/Appointments/toDoCountProviderModel.dart'; import 'package:hmg_patient_app/models/Appointments/toDoCountProviderModel.dart';
import 'package:hmg_patient_app/pages/appUpdatePage/app_update_page.dart'; import 'package:hmg_patient_app/pages/appUpdatePage/app_update_page.dart';
import 'package:hmg_patient_app/services/authentication/auth_provider.dart'; import 'package:hmg_patient_app/services/authentication/auth_provider.dart';
@ -80,18 +81,22 @@ class BaseAppClient {
if (body.containsKey('LanguageID')) { if (body.containsKey('LanguageID')) {
if (body['LanguageID'] != null) { if (body['LanguageID'] != null) {
//change this line because language issue happened on dental
body['LanguageID'] = body['LanguageID'] == 'ar' body['LanguageID'] = body['LanguageID'] == 'ar'
? 1 ? 1
: body['LanguageID'] == 'en' : body['LanguageID'] == 'en'
? 2 ? 2
: body['LanguageID']; : body['LanguageID'];
} else { } else {
body['LanguageID'] = Provider.of<ProjectViewModel>(AppGlobal.context, listen: false).isArabic ? 1 : 2; bool _isArabic = AppGlobal.context != null
? Provider.of<ProjectViewModel>(AppGlobal.context, listen: false).isArabic
: false;
body['LanguageID'] = _isArabic ? 1 : 2;
} }
} else { } else {
// body['LanguageID'] = (languageID.toString().toLowerCase() == 'ar' ? 1 : 2); bool _isArabic = AppGlobal.context != null
body['LanguageID'] = Provider.of<ProjectViewModel>(AppGlobal.context, listen: false).isArabic ? 1 : 2; ? Provider.of<ProjectViewModel>(AppGlobal.context, listen: false).isArabic
: false;
body['LanguageID'] = _isArabic ? 1 : 2;
} }
// } else { // } else {
// // body['LanguageID'] = (languageID.toString().toLowerCase() == 'ar' ? 1 : 2); // // body['LanguageID'] = (languageID.toString().toLowerCase() == 'ar' ? 1 : 2);
@ -209,19 +214,34 @@ class BaseAppClient {
// return; // return;
// if (await Utils.checkConnection(bypassConnectionCheck: bypassConnectionCheck)) { // if (await Utils.checkConnection(bypassConnectionCheck: bypassConnectionCheck)) {
if (await Utils.checkConnection(bypassConnectionCheck: true)) { if (await Utils.checkConnection(bypassConnectionCheck: true)) {
final _postStart = DateTime.now();
final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers); final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers);
final _postDuration = DateTime.now().difference(_postStart);
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
// Log every call regardless of success/failure
dynamic _loggedBody;
try { _loggedBody = json.decode(utf8.decode(response.bodyBytes)); } catch (_) { _loggedBody = response.body; }
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${_postStart.millisecondsSinceEpoch}',
timestamp: _postStart,
method: 'POST',
url: url,
endpoint: endPoint,
environment: ApiLogEntry.envFromUrl(url),
headers: headers,
requestBody: Map<String, dynamic>.from(body),
responseBody: _loggedBody,
statusCode: statusCode,
isSuccess: statusCode >= 200 && statusCode < 400,
duration: _postDuration,
));
if (statusCode < 200 || statusCode >= 400 || json == null) { if (statusCode < 200 || statusCode >= 400 || json == null) {
onFailure('Error While Fetching data', statusCode); onFailure('Error While Fetching data', statusCode);
logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode);
} else { } else {
// var decoded = utf8.decode(response.bodyBytes); var parsed = _loggedBody;
var parsed;
// if (url.contains('Services/NHIC.svc/REST/GetPatientInfo')) {
// parsed = json.decode(sampleNHICResponse);
// } else {
parsed = json.decode(utf8.decode(response.bodyBytes));
// }
// print("Response: $parsed"); // print("Response: $parsed");
@ -302,8 +322,20 @@ class BaseAppClient {
} }
} catch (e) { } catch (e) {
print(e); print(e);
print(e); // Log failed call so it still appears in API Logs
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${DateTime.now().millisecondsSinceEpoch}',
timestamp: DateTime.now(),
method: 'POST',
url: url,
endpoint: endPoint,
environment: ApiLogEntry.envFromUrl(url),
headers: const {},
requestBody: null,
responseBody: {'error': e.toString()},
statusCode: 0,
isSuccess: false,
));
if (e.toString().contains("ClientException")) { if (e.toString().contains("ClientException")) {
onFailure('Something went wrong, plase try again', -1); onFailure('Something went wrong, plase try again', -1);
_analytics.errorTracking.log("internet_connectivity", error: "no internet available"); _analytics.errorTracking.log("internet_connectivity", error: "no internet available");
@ -400,9 +432,27 @@ class BaseAppClient {
print("Headers : ${json.encode(headers)}"); print("Headers : ${json.encode(headers)}");
if (await Utils.checkConnection()) { if (await Utils.checkConnection()) {
final _ppStartTime = DateTime.now();
final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers); final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers);
final _ppDuration = DateTime.now().difference(_ppStartTime);
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
// print("statusCode :$statusCode"); // Log the API call
dynamic _ppParsedForLog;
try { _ppParsedForLog = json.decode(utf8.decode(response.bodyBytes)); } catch (_) { _ppParsedForLog = response.body; }
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${DateTime.now().millisecondsSinceEpoch}',
timestamp: _ppStartTime,
method: 'POST',
url: url,
endpoint: endPoint,
environment: ApiLogEntry.envFromUrl(url),
headers: headers,
requestBody: body != null ? Map<String, dynamic>.from(body) : null,
responseBody: _ppParsedForLog,
statusCode: statusCode,
isSuccess: statusCode >= 200 && statusCode < 400,
duration: _ppDuration,
));;
if (statusCode < 200 || statusCode >= 400 || json == null) { if (statusCode < 200 || statusCode >= 400 || json == null) {
onFailure!('Error While Fetching data', statusCode); onFailure!('Error While Fetching data', statusCode);
logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode);
@ -513,15 +563,33 @@ class BaseAppClient {
} }
debugPrint("URL : $url"); debugPrint("URL : $url");
// print("Body : ${json.encode(body)}");
if (await Utils.checkConnection()) { if (await Utils.checkConnection()) {
final _getHeaders = {'Content-Type': 'application/json', 'Accept': 'application/json'};
final _getStart = DateTime.now();
final response = await http.get( final response = await http.get(
Uri.parse(url.trim()), Uri.parse(url.trim()),
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, headers: _getHeaders,
); );
final _getDuration = DateTime.now().difference(_getStart);
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
// print("statusCode :$statusCode");
dynamic _getParsedForLog;
try { _getParsedForLog = json.decode(utf8.decode(response.bodyBytes)); } catch (_) { _getParsedForLog = response.body; }
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${DateTime.now().millisecondsSinceEpoch}',
timestamp: _getStart,
method: 'GET',
url: url,
endpoint: endPoint,
environment: ApiLogEntry.envFromUrl(url),
headers: _getHeaders,
requestBody: queryParams,
responseBody: _getParsedForLog,
statusCode: statusCode,
isSuccess: statusCode >= 200 && statusCode < 400,
duration: _getDuration,
));;
if (statusCode < 200 || statusCode >= 400 || json == null) { if (statusCode < 200 || statusCode >= 400 || json == null) {
onFailure!('Error While Fetching data', statusCode); onFailure!('Error While Fetching data', statusCode);
@ -557,20 +625,37 @@ class BaseAppClient {
} }
print("URL : $url"); print("URL : $url");
var ss = json.encode(queryParams);
if (await Utils.checkConnection()) { if (await Utils.checkConnection()) {
final response = await http.get(Uri.parse(url.trim()), headers: { final Map <String, String> _gpHeaders = {
'Content-Type': 'text/html; charset=utf-8', 'Content-Type': 'text/html; charset=utf-8',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': token ?? '', 'Authorization': token ?? '',
'Mobilenumber': user != null ? Utils.getPhoneNumberWithoutZero(user['MobileNumber'].toString()) : "", 'Mobilenumber': user != null ? Utils.getPhoneNumberWithoutZero(user['MobileNumber'].toString()) : "",
'Statictoken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', 'Statictoken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
'Username': user != null ? user['PatientID'].toString() : "", 'Username': user != null ? user['PatientID'].toString() : "",
// 'Host': "mdlaboratories.com", };
}); final _gpStart = DateTime.now();
final response = await http.get(Uri.parse(url.trim()), headers: _gpHeaders);
final _gpDuration = DateTime.now().difference(_gpStart);
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
// print("statusCode :$statusCode");
dynamic _gpParsedForLog;
try { _gpParsedForLog = json.decode(utf8.decode(response.bodyBytes)); } catch (_) { _gpParsedForLog = response.body; }
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${DateTime.now().millisecondsSinceEpoch}',
timestamp: _gpStart,
method: 'GET',
url: url,
endpoint: endPoint,
environment: ApiLogEntry.envFromUrl(url),
headers: _gpHeaders,
requestBody: queryParams,
responseBody: _gpParsedForLog,
statusCode: statusCode,
isSuccess: statusCode >= 200 && statusCode < 400,
duration: _gpDuration,
));;
if (statusCode < 200 || statusCode >= 400 || json == null) { if (statusCode < 200 || statusCode >= 400 || json == null) {
if (statusCode == 401) { if (statusCode == 401) {
@ -607,13 +692,32 @@ class BaseAppClient {
if (await Utils.checkConnection()) { if (await Utils.checkConnection()) {
headers!.addAll({'Content-Type': 'application/json', 'Accept': 'application/json'}); headers!.addAll({'Content-Type': 'application/json', 'Accept': 'application/json'});
final _spStart = DateTime.now();
final response = await http.post( final response = await http.post(
Uri.parse(url.trim()), Uri.parse(url.trim()),
body: json.encode(body), body: json.encode(body),
headers: headers, headers: headers,
); );
final _spDuration = DateTime.now().difference(_spStart);
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
// print("statusCode :$statusCode");
dynamic _spLogBody;
try { _spLogBody = json.decode(response.body); } catch (_) { _spLogBody = response.body; }
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${_spStart.millisecondsSinceEpoch}',
timestamp: _spStart,
method: 'POST',
url: fullUrl,
endpoint: fullUrl,
environment: ApiLogEntry.envFromUrl(fullUrl),
headers: headers,
requestBody: body.map((k, v) => MapEntry(k.toString(), v)),
responseBody: _spLogBody,
statusCode: statusCode,
isSuccess: statusCode >= 200 && statusCode < 400,
duration: _spDuration,
));
if (await handleUnauthorized(statusCode, forUrl: fullUrl)) simplePost(fullUrl, onFailure: onFailure, onSuccess: onSuccess, body: body, headers: headers); if (await handleUnauthorized(statusCode, forUrl: fullUrl)) simplePost(fullUrl, onFailure: onFailure, onSuccess: onSuccess, body: body, headers: headers);
// print(response.body.toString()); // print(response.body.toString());
@ -644,13 +748,31 @@ class BaseAppClient {
if (await Utils.checkConnection()) { if (await Utils.checkConnection()) {
headers.addAll({'Content-Type': 'application/json', 'Accept': 'application/json'}); headers.addAll({'Content-Type': 'application/json', 'Accept': 'application/json'});
final _sgStart = DateTime.now();
final response = await http.get( final response = await http.get(
Uri.parse(url.trim()), Uri.parse(url.trim()),
headers: headers, headers: headers,
); );
final _sgDuration = DateTime.now().difference(_sgStart);
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
// print("statusCode :$statusCode");
dynamic _sgLogBody;
try { _sgLogBody = json.decode(response.body); } catch (_) { _sgLogBody = response.body; }
if (ENABLE_DEV_TOOLS) ApiLoggerService.instance.addLog(ApiLogEntry(
id: '${_sgStart.millisecondsSinceEpoch}',
timestamp: _sgStart,
method: 'GET',
url: url,
endpoint: fullUrl,
environment: ApiLogEntry.envFromUrl(url),
headers: headers,
requestBody: queryParams,
responseBody: _sgLogBody,
statusCode: statusCode,
isSuccess: statusCode >= 200 && statusCode < 400,
duration: _sgDuration,
));
if (await handleUnauthorized(statusCode, forUrl: fullUrl)) simpleGet(fullUrl, onFailure: onFailure, onSuccess: onSuccess, headers: headers, queryParams: queryParams); if (await handleUnauthorized(statusCode, forUrl: fullUrl)) simpleGet(fullUrl, onFailure: onFailure, onSuccess: onSuccess, headers: headers, queryParams: queryParams);
if (statusCode < 200 || statusCode >= 400 || json == null) { if (statusCode < 200 || statusCode >= 400 || json == null) {

@ -0,0 +1,383 @@
// filepath: lib/pages/dev_tools/api_log_screen.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app/dev_tools/api_logger_service.dart';
import 'package:hmg_patient_app/widgets/others/app_scaffold_widget.dart';
class ApiLogScreen extends StatefulWidget {
const ApiLogScreen({Key? key}) : super(key: key);
@override
State<ApiLogScreen> createState() => _ApiLogScreenState();
}
class _ApiLogScreenState extends State<ApiLogScreen> {
final ApiLoggerService _logger = ApiLoggerService.instance;
String _searchQuery = '';
@override
Widget build(BuildContext context) {
return AppScaffold(
showNewAppBar: true,
showNewAppBarTitle: true,
isShowDecPage: false,
appBarTitle: 'API Logs',
appBarIcons: [
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.black),
tooltip: 'Clear all logs',
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Clear Logs'),
content: const Text('Are you sure you want to clear all API logs?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Clear', style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) _logger.clear();
},
),
],
body: AnimatedBuilder(
animation: _logger,
builder: (context, _) {
final logs = _searchQuery.isEmpty
? _logger.logs
: _logger.logs
.where((l) =>
l.endpoint.toLowerCase().contains(_searchQuery.toLowerCase()) || l.url.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
return Column(
children: [
// Search Bar
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
decoration: InputDecoration(
hintText: 'Search endpoint or URL…',
prefixIcon: const Icon(Icons.search, size: 20),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300)),
filled: true,
fillColor: Colors.grey.shade50,
),
onChanged: (v) => setState(() => _searchQuery = v),
),
),
// Stats bar
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
color: Colors.grey.shade100,
child: Row(
children: [
Text('${logs.length} calls', style: const TextStyle(fontSize: 12, color: Colors.grey)),
const Spacer(),
Text(
'${logs.where((l) => l.isSuccess).length} success '
'${logs.where((l) => !l.isSuccess).length} failed',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
Expanded(
child: logs.isEmpty
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.api_outlined, size: 60, color: Colors.grey),
SizedBox(height: 12),
Text('No API calls recorded yet.', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.separated(
padding: const EdgeInsets.only(bottom: 80),
itemCount: logs.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final entry = logs[index];
return _ApiLogTile(entry: entry);
},
),
),
],
);
},
),
);
}
}
// Individual expandable log tile
class _ApiLogTile extends StatefulWidget {
final ApiLogEntry entry;
const _ApiLogTile({required this.entry});
@override
State<_ApiLogTile> createState() => _ApiLogTileState();
}
class _ApiLogTileState extends State<_ApiLogTile> {
bool _expanded = false;
int _tabIndex = 0; // 0=Request, 1=Response, 2=Headers
@override
Widget build(BuildContext context) {
final e = widget.entry;
final statusColor = e.isSuccess ? const Color(0xFF27AE60) : const Color(0xFFE74C3C);
final methodColor = e.method == 'GET' ? const Color(0xFF2980B9) : const Color(0xFF8E44AD);
return Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Collapsed header
InkWell(
onTap: () => setState(() => _expanded = !_expanded),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// Method badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: methodColor.withAlpha(25),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: methodColor.withAlpha(100))),
child: Text(e.method, style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: methodColor)),
),
const SizedBox(width: 8),
// Status badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: statusColor.withAlpha(25), borderRadius: BorderRadius.circular(4)),
child: Text(e.statusCode == 0 ? 'ERR' : e.statusCode.toString(),
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: statusColor)),
),
const SizedBox(width: 6),
// Environment badge
if (e.environment.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withAlpha(20),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFF6C5CE7).withAlpha(80))),
child: Text(e.environment, style: const TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFF6C5CE7))),
),
const SizedBox(width: 6),
// Endpoint
Expanded(
child: Text(
e.endpoint,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF2B353E)),
overflow: TextOverflow.ellipsis,
),
),
// Time
Text(
'${e.timestamp.hour.toString().padLeft(2, '0')}:${e.timestamp.minute.toString().padLeft(2, '0')}:${e.timestamp.second.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
const SizedBox(width: 4),
Icon(_expanded ? Icons.expand_less : Icons.expand_more, size: 18, color: Colors.grey),
],
),
),
),
// Expanded detail
if (_expanded) ...[
// Full URL
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
color: Colors.grey.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.link, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(e.url, style: const TextStyle(fontSize: 11, color: Colors.grey), overflow: TextOverflow.ellipsis, maxLines: 2),
),
IconButton(
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
icon: const Icon(Icons.copy, size: 14, color: Colors.grey),
onPressed: () {
Clipboard.setData(ClipboardData(text: e.url));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('URL copied'), duration: Duration(seconds: 1)));
},
),
],
),
if (e.duration != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text('Duration: ${e.duration!.inMilliseconds}ms', style: const TextStyle(fontSize: 11, color: Colors.grey)),
),
],
),
),
// Tab selector
Container(
color: Colors.white,
child: Row(
children: [
_TabChip(label: 'Request', selected: _tabIndex == 0, onTap: () => setState(() => _tabIndex = 0)),
_TabChip(label: 'Response', selected: _tabIndex == 1, onTap: () => setState(() => _tabIndex = 1)),
_TabChip(label: 'Headers', selected: _tabIndex == 2, onTap: () => setState(() => _tabIndex = 2)),
],
),
),
// Content area
Container(
width: double.infinity,
constraints: const BoxConstraints(maxHeight: 280),
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF1E1E1E),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: SelectableText(
_tabIndex == 0
? e.prettyRequestBody
: _tabIndex == 1
? e.prettyResponseBody
: e.headers.entries.map((h) => '${h.key}: ${h.value}').join('\n'),
style: const TextStyle(fontFamily: 'monospace', fontSize: 11, color: Color(0xFFD4D4D4)),
),
),
),
// Export buttons
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: Row(
children: [
_ExportButton(
label: 'Copy Formatted',
icon: Icons.text_snippet_outlined,
onTap: () {
Clipboard.setData(ClipboardData(text: e.toFormattedText()));
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Copied as formatted text'), duration: Duration(seconds: 1)));
},
),
const SizedBox(width: 8),
_ExportButton(
label: 'Copy as cURL',
icon: Icons.terminal,
onTap: () {
Clipboard.setData(ClipboardData(text: e.toCurl()));
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Copied as cURL command'), duration: Duration(seconds: 1)));
},
),
const SizedBox(width: 8),
_ExportButton(
label: 'Copy JSON',
icon: Icons.data_object,
onTap: () {
final payload = {
'method': e.method,
'url': e.url,
'headers': e.headers,
'request': e.requestBody,
'response': e.responseBody,
'statusCode': e.statusCode,
'timestamp': e.timestamp.toIso8601String(),
};
Clipboard.setData(ClipboardData(text: const JsonEncoder.withIndent(' ').convert(payload)));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Copied as JSON'), duration: Duration(seconds: 1)));
},
),
],
),
),
],
],
),
);
}
}
class _TabChip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback onTap;
const _TabChip({required this.label, required this.selected, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(left: 12, bottom: 6, top: 6),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: selected ? const Color(0xFF2B353E) : Colors.grey.shade200,
borderRadius: BorderRadius.circular(20),
),
child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: selected ? Colors.white : Colors.black54)),
),
);
}
}
class _ExportButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback onTap;
const _ExportButton({required this.label, required this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
return Expanded(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(6),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: const Color(0xFF2B353E)),
const SizedBox(height: 2),
Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFF2B353E)), textAlign: TextAlign.center),
],
),
),
),
);
}
}

@ -0,0 +1,133 @@
// filepath: lib/core/service/client/api_logger_service.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
/// Holds the details of a single API call.
class ApiLogEntry {
final String id;
final DateTime timestamp;
final String method;
final String url;
final String endpoint;
final String environment;
final Map<String, String> headers;
final Map<String, dynamic>? requestBody;
final dynamic responseBody;
final int statusCode;
final bool isSuccess;
final Duration? duration;
ApiLogEntry({
required this.id,
required this.timestamp,
required this.method,
required this.url,
required this.endpoint,
this.environment = '',
required this.headers,
this.requestBody,
this.responseBody,
required this.statusCode,
required this.isSuccess,
this.duration,
});
/// Derives a short environment label from a full URL string.
static String envFromUrl(String url) {
if (url.contains('vidamergeuat')) return 'VidaMerge';
if (url.contains('vidauat')) return 'VidaPlus';
if (url.contains('orash')) return 'Orash';
if (url.contains('uat.hmgwebservices')) return 'UAT';
if (url.contains('rc.hmg.com') && url.contains('/test')) return 'RC-Test';
if (url.contains('ms.hmg.com/rc')) return 'MS-RC';
if (url.contains('rc.hmg.com')) return 'RC';
if (url.contains('mdlaboratories.com/exacartapitest')) return 'Pharmacy-Test';
if (url.contains('mdlaboratories.com')) return 'Pharmacy';
if (url.contains('hmgwebservices.com')) return 'Production';
// External / other
final uri = Uri.tryParse(url);
return uri?.host.isNotEmpty == true ? uri!.host.split('.').first : 'External';
}
/// Returns a pretty-printed JSON string of the request body.
String get prettyRequestBody {
if (requestBody == null) return '(none)';
try {
return const JsonEncoder.withIndent(' ').convert(requestBody);
} catch (_) {
return requestBody.toString();
}
}
/// Returns a pretty-printed JSON string of the response body.
String get prettyResponseBody {
if (responseBody == null) return '(none)';
try {
if (responseBody is String) {
final decoded = json.decode(responseBody as String);
return const JsonEncoder.withIndent(' ').convert(decoded);
}
return const JsonEncoder.withIndent(' ').convert(responseBody);
} catch (_) {
return responseBody.toString();
}
}
/// Returns formatted text report (headers + request + response sectioned).
String toFormattedText() {
final buf = StringBuffer();
buf.writeln('═══════════════════════════════════');
buf.writeln('[$method] $url');
buf.writeln('Status : $statusCode | ${isSuccess ? '✅ Success' : '❌ Failed'}');
if (environment.isNotEmpty) buf.writeln('Env : $environment');
buf.writeln('Time : ${timestamp.toIso8601String()}');
if (duration != null) buf.writeln('Duration: ${duration!.inMilliseconds}ms');
buf.writeln('───────────────── HEADERS ─────────────────');
headers.forEach((k, v) => buf.writeln('$k: $v'));
buf.writeln('───────────────── REQUEST ─────────────────');
buf.writeln(prettyRequestBody);
buf.writeln('──────────────── RESPONSE ─────────────────');
buf.writeln(prettyResponseBody);
buf.writeln('═══════════════════════════════════');
return buf.toString();
}
/// Returns a cURL command equivalent.
String toCurl() {
final buf = StringBuffer();
buf.write("curl -X $method \\\n");
buf.write(" '${url.trim()}' \\\n");
headers.forEach((k, v) {
buf.write(" -H '$k: $v' \\\n");
});
if (requestBody != null && method != 'GET') {
final bodyStr = json.encode(requestBody).replaceAll("'", "\\'");
buf.write(" -d '$bodyStr'");
}
return buf.toString();
}
}
/// Singleton service that collects API call logs in-memory.
class ApiLoggerService extends ChangeNotifier {
ApiLoggerService._internal();
static final ApiLoggerService instance = ApiLoggerService._internal();
final List<ApiLogEntry> _logs = [];
List<ApiLogEntry> get logs => List.unmodifiable(_logs.reversed.toList());
void addLog(ApiLogEntry entry) {
_logs.add(entry);
// Keep last 500 entries to avoid huge memory usage
if (_logs.length > 500) _logs.removeAt(0);
notifyListeners();
}
void clear() {
_logs.clear();
notifyListeners();
}
}

@ -0,0 +1,168 @@
// filepath: lib/config/environment_config.dart
import 'package:hmg_patient_app/config/config.dart';
import 'package:hmg_patient_app/uitl/app_shared_preferences.dart';
// Keys for SharedPreferences
const ENV_BASE_URL_KEY = 'env_base_url';
const ENV_RC_BASE_URL_KEY = 'env_rc_base_url';
const ENV_PHARMACY_BASE_URL_KEY = 'env_pharmacy_base_url';
const ENV_EXA_CART_BASE_URL_KEY = 'env_exa_cart_base_url';
const ENV_PAYFORT_KEY = 'env_payfort';
// Preset environments
class AppEnvironmentPreset {
final String label;
final String baseUrl;
final String rcBaseUrl;
final String pharmacyBaseUrl;
final String exaCartBaseUrl;
final String payfortMode; // 'production' | 'sandbox'
const AppEnvironmentPreset({
required this.label,
required this.baseUrl,
required this.rcBaseUrl,
required this.pharmacyBaseUrl,
required this.exaCartBaseUrl,
required this.payfortMode,
});
}
const List<AppEnvironmentPreset> kEnvironmentPresets = [
AppEnvironmentPreset(
label: 'Production',
baseUrl: 'https://hmgwebservices.com/',
rcBaseUrl: 'https://rc.hmg.com/',
pharmacyBaseUrl: 'https://mdlaboratories.com/exacartapi/api/',
exaCartBaseUrl: 'https://mdlaboratories.com/offersdiscounts',
payfortMode: 'production',
),
AppEnvironmentPreset(
label: 'UAT',
baseUrl: 'https://uat.hmgwebservices.com/',
rcBaseUrl: 'https://rc.hmg.com/test/',
pharmacyBaseUrl: 'https://uat.hmgwebservices.com/epharmacy/api/',
exaCartBaseUrl: 'https://mdlaboratories.com/offersdiscounts',
payfortMode: 'sandbox',
),
AppEnvironmentPreset(
label: 'VidaPlus UAT',
baseUrl: 'https://vidauat.cloudsolutions.com.sa/',
rcBaseUrl: 'https://rc.hmg.com/',
pharmacyBaseUrl: 'https://mdlaboratories.com/exacartapitest/api/',
exaCartBaseUrl: 'https://mdlaboratories.com/offersdiscounts',
payfortMode: 'sandbox',
),
AppEnvironmentPreset(
label: 'VidaMerge UAT',
baseUrl: 'https://vidamergeuat.cloudsolutions.com.sa/',
rcBaseUrl: 'https://rc.hmg.com/',
pharmacyBaseUrl: 'https://mdlaboratories.com/exacartapitest/api/',
exaCartBaseUrl: 'https://mdlaboratories.com/offersdiscounts',
payfortMode: 'sandbox',
),
AppEnvironmentPreset(
label: 'Orash UAT',
baseUrl: 'https://orash.cloudsolutions.com.sa/',
rcBaseUrl: 'https://rc.hmg.com/',
pharmacyBaseUrl: 'https://mdlaboratories.com/exacartapi/api/',
exaCartBaseUrl: 'https://mdlaboratories.com/offersdiscounts',
payfortMode: 'sandbox',
),
AppEnvironmentPreset(
label: 'MS HMG RC',
baseUrl: 'https://hmgwebservices.com/',
rcBaseUrl: 'https://ms.hmg.com/rc/',
pharmacyBaseUrl: 'https://mdlaboratories.com/exacartapi/api/',
exaCartBaseUrl: 'https://mdlaboratories.com/offersdiscounts',
payfortMode: 'production',
),
];
// Service
class EnvironmentConfigService {
static final EnvironmentConfigService instance = EnvironmentConfigService._internal();
EnvironmentConfigService._internal();
final AppSharedPreferences _prefs = AppSharedPreferences();
/// Apply stored environment or fall back to production defaults.
Future<void> applyStoredEnvironment() async {
final storedBase = await _prefs.getString(ENV_BASE_URL_KEY);
final storedRc = await _prefs.getString(ENV_RC_BASE_URL_KEY);
final storedPharmacy = await _prefs.getString(ENV_PHARMACY_BASE_URL_KEY);
final storedExaCart = await _prefs.getString(ENV_EXA_CART_BASE_URL_KEY);
if (storedBase != null && storedBase.isNotEmpty) BASE_URL = storedBase;
if (storedRc != null && storedRc.isNotEmpty) RC_BASE_URL = storedRc;
if (storedPharmacy != null && storedPharmacy.isNotEmpty) {
BASE_PHARMACY_URL = storedPharmacy;
PHARMACY_BASE_URL = storedPharmacy;
}
if (storedExaCart != null && storedExaCart.isNotEmpty) EXA_CART_API_BASE_URL = storedExaCart;
}
/// Save an environment preset and apply it immediately to the global config vars.
Future<void> applyPreset(AppEnvironmentPreset preset) async {
await _prefs.setString(ENV_BASE_URL_KEY, preset.baseUrl);
await _prefs.setString(ENV_RC_BASE_URL_KEY, preset.rcBaseUrl);
await _prefs.setString(ENV_PHARMACY_BASE_URL_KEY, preset.pharmacyBaseUrl);
await _prefs.setString(ENV_EXA_CART_BASE_URL_KEY, preset.exaCartBaseUrl);
await _prefs.setString(ENV_PAYFORT_KEY, preset.payfortMode);
BASE_URL = preset.baseUrl;
RC_BASE_URL = preset.rcBaseUrl;
BASE_PHARMACY_URL = preset.pharmacyBaseUrl;
PHARMACY_BASE_URL = preset.pharmacyBaseUrl;
EXA_CART_API_BASE_URL = preset.exaCartBaseUrl;
}
/// Clears all saved environment prefs so the app falls back to the
/// hardcoded production URLs defined in config.dart.
Future<void> resetToDefault() async {
await _prefs.remove(ENV_BASE_URL_KEY);
await _prefs.remove(ENV_RC_BASE_URL_KEY);
await _prefs.remove(ENV_PHARMACY_BASE_URL_KEY);
await _prefs.remove(ENV_EXA_CART_BASE_URL_KEY);
await _prefs.remove(ENV_PAYFORT_KEY);
// Reset runtime vars back to the values compiled in config.dart
BASE_URL = 'https://hmgwebservices.com/';
RC_BASE_URL = 'https://rc.hmg.com/';
BASE_PHARMACY_URL = 'https://mdlaboratories.com/exacartapi/api/';
PHARMACY_BASE_URL = 'https://mdlaboratories.com/exacartapi/api/';
EXA_CART_API_BASE_URL = 'https://mdlaboratories.com/offersdiscounts';
}
Future<String> getCurrentBaseUrl() async {
return (await _prefs.getString(ENV_BASE_URL_KEY)) ?? BASE_URL;
}
Future<String> getCurrentRcUrl() async {
return (await _prefs.getString(ENV_RC_BASE_URL_KEY)) ?? RC_BASE_URL;
}
Future<String> getCurrentPharmacyUrl() async {
return (await _prefs.getString(ENV_PHARMACY_BASE_URL_KEY)) ?? PHARMACY_BASE_URL;
}
Future<String> getCurrentExaCartUrl() async {
return (await _prefs.getString(ENV_EXA_CART_BASE_URL_KEY)) ?? EXA_CART_API_BASE_URL;
}
Future<String> getCurrentPayfortMode() async {
return (await _prefs.getString(ENV_PAYFORT_KEY)) ?? 'production';
}
/// Find preset whose baseUrl matches the currently saved one.
Future<AppEnvironmentPreset?> getCurrentPreset() async {
final storedBase = await getCurrentBaseUrl();
try {
return kEnvironmentPresets.firstWhere((p) => p.baseUrl == storedBase);
} catch (_) {
return null;
}
}
}

@ -0,0 +1,350 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app/dev_tools/environment_config.dart';
/// Public class can be used directly with showModalBottomSheet.
class EnvironmentConfigSheet extends StatefulWidget {
const EnvironmentConfigSheet({Key? key}) : super(key: key);
@override
State<EnvironmentConfigSheet> createState() => _EnvironmentConfigSheetState();
}
/// Convenience function opens the sheet and returns true if app restart needed.
Future<bool> showEnvironmentConfigSheet(BuildContext context) async {
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const EnvironmentConfigSheet(),
);
return result ?? false;
}
class _EnvironmentConfigSheetState extends State<EnvironmentConfigSheet> {
final _envService = EnvironmentConfigService.instance;
AppEnvironmentPreset? _selectedPreset;
bool _isSaving = false;
@override
void initState() {
super.initState();
_loadCurrent();
}
Future<void> _loadCurrent() async {
final preset = await _envService.getCurrentPreset();
if (!mounted) return;
setState(() {
// If no preset is stored, fall back to production (first preset)
_selectedPreset = preset ?? kEnvironmentPresets.first;
});
}
void _selectPreset(AppEnvironmentPreset p) {
setState(() => _selectedPreset = p);
}
Future<void> _save() async {
if (_selectedPreset == null) return;
setState(() => _isSaving = true);
try {
await _envService.applyPreset(_selectedPreset!);
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
setState(() => _isSaving = false);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to save: $e')));
}
}
Future<void> _resetToDefault() async {
setState(() => _isSaving = true);
try {
await _envService.resetToDefault();
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
setState(() => _isSaving = false);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to reset: $e')));
}
}
@override
Widget build(BuildContext context) {
final mq = MediaQuery.of(context);
final p = _selectedPreset;
return Container(
height: mq.size.height * 0.85,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Handle bar
Container(
margin: const EdgeInsets.only(top: 12, bottom: 4),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2)),
),
// Title row
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
child: Row(
children: [
const Icon(Icons.tune, color: Color(0xFF2B353E)),
const SizedBox(width: 10),
const Expanded(
child: Text('Environment Configuration',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFF2B353E))),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(false)),
],
),
),
const Divider(height: 1),
// Scrollable body
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Preset chips
const _SectionLabel('Select Environment'),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: kEnvironmentPresets
.map((preset) => _Chip(
label: preset.label,
selected: _selectedPreset?.label == preset.label,
onTap: () => _selectPreset(preset),
))
.toList(),
),
const SizedBox(height: 24),
// URL preview (read-only)
if (p != null) ...[
const _SectionLabel('URLs for selected environment'),
const SizedBox(height: 10),
_UrlDisplay(label: 'Main Base URL', value: p.baseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'RC Base URL', value: p.rcBaseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'Pharmacy Base URL', value: p.pharmacyBaseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'ExaCart (Offers) URL', value: p.exaCartBaseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'PayFort Mode', value: p.payfortMode),
const SizedBox(height: 24),
],
// Reset to Default
InkWell(
onTap: _isSaving ? null : _resetToDefault,
borderRadius: BorderRadius.circular(10),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE74C3C)),
borderRadius: BorderRadius.circular(10),
color: const Color(0xFFFFF5F5)),
child: Row(
children: const [
Icon(Icons.restore,
color: Color(0xFFE74C3C), size: 20),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Reset to Default',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Color(0xFFE74C3C))),
SizedBox(height: 2),
Text(
'Clears saved config and uses the URLs defined in code (Production)',
style: TextStyle(
fontSize: 11,
color: Color(0xFFE74C3C))),
],
),
),
],
),
),
),
const SizedBox(height: 20),
// Warning
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF3CD),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD700))),
child: const Row(children: [
Icon(Icons.warning_amber_rounded,
color: Color(0xFFE6A817), size: 18),
SizedBox(width: 8),
Expanded(
child: Text(
'After saving, the app will restart to apply the new environment.',
style: TextStyle(
fontSize: 12, color: Color(0xFF856404)),
),
),
]),
),
const SizedBox(height: 80),
],
),
),
),
// Save button
Container(
padding:
EdgeInsets.fromLTRB(20, 12, 20, 12 + mq.padding.bottom),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(15),
blurRadius: 8,
offset: const Offset(0, -2))
],
),
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2B353E),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: _isSaving ? null : _save,
child: _isSaving
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Text('Save & Restart App',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white)),
),
),
),
],
),
);
}
}
// Small private helpers
class _SectionLabel extends StatelessWidget {
final String text;
const _SectionLabel(this.text);
@override
Widget build(BuildContext context) => Text(text,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Color(0xFF2B353E)));
}
class _Chip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback onTap;
const _Chip(
{required this.label, required this.selected, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: selected ? const Color(0xFF2B353E) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected
? const Color(0xFF2B353E)
: Colors.grey.shade300,
width: selected ? 2 : 1),
),
child: Text(label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: selected ? Colors.white : Colors.black87)),
),
);
}
}
class _UrlDisplay extends StatelessWidget {
final String label;
final String value;
const _UrlDisplay({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.grey)),
const SizedBox(height: 3),
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Text(value,
style: const TextStyle(
fontSize: 12, color: Color(0xFF2B353E))),
),
],
);
}
}

@ -23,6 +23,7 @@ import 'package:localstorage/localstorage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'config/size_config.dart'; import 'config/size_config.dart';
import 'dev_tools/environment_config.dart';
import 'core/viewModels/auth_provider_view_model.dart'; import 'core/viewModels/auth_provider_view_model.dart';
import 'core/viewModels/pharmacyModule/OrderPreviewViewModel.dart'; import 'core/viewModels/pharmacyModule/OrderPreviewViewModel.dart';
import 'core/viewModels/project_view_model.dart'; import 'core/viewModels/project_view_model.dart';
@ -44,6 +45,7 @@ void main() async {
// }; // };
setupLocator(); setupLocator();
if (ENABLE_DEV_TOOLS) await EnvironmentConfigService.instance.applyStoredEnvironment();
HttpOverrides.global = MyHttpOverrides(); HttpOverrides.global = MyHttpOverrides();
runApp(MyApp()); runApp(MyApp());

@ -1,15 +1,30 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:flutter_amazonpaymentservices/environment_type.dart';
// import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
// import 'package:flutter_amazonpaymentservices/environment_type.dart';
// import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
// import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_patient_app/analytics/google-analytics.dart'; import 'package:hmg_patient_app/analytics/google-analytics.dart';
import 'package:hmg_patient_app/config/config.dart'; import 'package:hmg_patient_app/config/config.dart';
import 'package:hmg_patient_app/config/shared_pref_kay.dart'; import 'package:hmg_patient_app/config/shared_pref_kay.dart';
import 'package:hmg_patient_app/config/size_config.dart';
import 'package:hmg_patient_app/core/service/AuthenticatedUserObject.dart'; import 'package:hmg_patient_app/core/service/AuthenticatedUserObject.dart';
import 'package:hmg_patient_app/core/service/medical/vital_sign_service.dart'; import 'package:hmg_patient_app/core/service/medical/vital_sign_service.dart';
import 'package:hmg_patient_app/core/service/privilege_service.dart'; import 'package:hmg_patient_app/core/service/privilege_service.dart';
import 'package:hmg_patient_app/core/viewModels/appointment_rate_view_model.dart'; import 'package:hmg_patient_app/core/viewModels/appointment_rate_view_model.dart';
import 'package:hmg_patient_app/core/viewModels/pharmacyModule/pharmacy_module_view_model.dart'; import 'package:hmg_patient_app/core/viewModels/pharmacyModule/pharmacy_module_view_model.dart';
import 'package:hmg_patient_app/core/viewModels/project_view_model.dart'; import 'package:hmg_patient_app/core/viewModels/project_view_model.dart';
import 'package:hmg_patient_app/dev_tools/api_log_screen.dart';
import 'package:hmg_patient_app/dev_tools/environment_config_sheet.dart';
import 'package:hmg_patient_app/locator.dart';
import 'package:hmg_patient_app/models/Appointments/toDoCountProviderModel.dart'; import 'package:hmg_patient_app/models/Appointments/toDoCountProviderModel.dart';
import 'package:hmg_patient_app/models/Authentication/authenticated_user.dart'; import 'package:hmg_patient_app/models/Authentication/authenticated_user.dart';
import 'package:hmg_patient_app/models/Authentication/check_activation_code_response.dart'; import 'package:hmg_patient_app/models/Authentication/check_activation_code_response.dart';
@ -37,29 +52,17 @@ import 'package:hmg_patient_app/uitl/LocalNotification.dart';
import 'package:hmg_patient_app/uitl/app_shared_preferences.dart'; import 'package:hmg_patient_app/uitl/app_shared_preferences.dart';
import 'package:hmg_patient_app/uitl/app_toast.dart'; import 'package:hmg_patient_app/uitl/app_toast.dart';
import 'package:hmg_patient_app/uitl/gif_loader_dialog_utils.dart'; import 'package:hmg_patient_app/uitl/gif_loader_dialog_utils.dart';
import 'package:hmg_patient_app/uitl/navigation_service.dart';
import 'package:hmg_patient_app/uitl/translations_delegate_base.dart'; import 'package:hmg_patient_app/uitl/translations_delegate_base.dart';
import 'package:hmg_patient_app/uitl/utils.dart'; import 'package:hmg_patient_app/uitl/utils.dart';
import 'package:hmg_patient_app/uitl/utils_new.dart'; import 'package:hmg_patient_app/uitl/utils_new.dart';
import 'package:hmg_patient_app/widgets/dialogs/confirm_dialog.dart'; import 'package:hmg_patient_app/widgets/dialogs/confirm_dialog.dart';
import 'package:hmg_patient_app/widgets/text/app_texts_widget.dart'; import 'package:hmg_patient_app/widgets/text/app_texts_widget.dart';
import 'package:hmg_patient_app/widgets/transitions/fade_page.dart'; import 'package:hmg_patient_app/widgets/transitions/fade_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:flutter_amazonpaymentservices/environment_type.dart';
// import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
// import 'package:flutter_amazonpaymentservices/environment_type.dart';
// import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
// import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../config/size_config.dart';
import '../../locator.dart';
import 'drawer_item_widget.dart'; import 'drawer_item_widget.dart';
class AppDrawer extends StatefulWidget { class AppDrawer extends StatefulWidget {
@ -144,8 +147,15 @@ class _AppDrawerState extends State<AppDrawer> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text( Text(
(user != null && projectProvider!.isLogin) ? user!.firstName! + ' ' + user!.lastName! : TranslationBase.of(context).cantSeeProfile, (user != null && projectProvider!.isLogin)
style: TextStyle(color: Color(0xff2E303A), fontWeight: FontWeight.bold, fontSize: 20, letterSpacing: -1.2, height: 35 / 20), ? user!.firstName! + ' ' + user!.lastName!
: TranslationBase.of(context).cantSeeProfile,
style: TextStyle(
color: Color(0xff2E303A),
fontWeight: FontWeight.bold,
fontSize: 20,
letterSpacing: -1.2,
height: 35 / 20),
), ),
AutoSizeText( AutoSizeText(
(user != null && projectProvider!.isLogin) (user != null && projectProvider!.isLogin)
@ -173,7 +183,11 @@ class _AppDrawerState extends State<AppDrawer> {
// ), // ),
mHeight(20.0), mHeight(20.0),
(user != null && projectProvider!.isLogin) (user != null && projectProvider!.isLogin)
? Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${user!.patientID.toString()}", fit: BoxFit.fill, height: 70, width: 70) ? Image.network(
"https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${user!.patientID.toString()}",
fit: BoxFit.fill,
height: 70,
width: 70)
: Container(), : Container(),
], ],
), ),
@ -216,7 +230,8 @@ class _AppDrawerState extends State<AppDrawer> {
children: <Widget>[ children: <Widget>[
(user!.isFamily == null || user!.isFamily == false) (user!.isFamily == null || user!.isFamily == false)
? InkWell( ? InkWell(
child: DrawerItem(TranslationBase.of(context).family, SvgPicture.asset("assets/images/new/family_files.svg"), child: DrawerItem(
TranslationBase.of(context).family, SvgPicture.asset("assets/images/new/family_files.svg"),
isImageIcon: true, isImageIcon: true,
bottomLine: false, bottomLine: false,
textColor: CustomColors.black, textColor: CustomColors.black,
@ -306,7 +321,10 @@ class _AppDrawerState extends State<AppDrawer> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Icon(Icons.person, color: result.responseID == user!.patientID ? Colors.transparent : Colors.transparent), child: Icon(Icons.person,
color: result.responseID == user!.patientID
? Colors.transparent
: Colors.transparent),
), ),
Expanded( Expanded(
flex: 7, flex: 7,
@ -317,14 +335,20 @@ class _AppDrawerState extends State<AppDrawer> {
children: <Widget>[ children: <Widget>[
AppText( AppText(
result.patientName!, result.patientName!,
color: result.responseID == user!.patientID ? Color(0xffC5272D) : Color(0xff2B353E), color: result.responseID == user!.patientID
? Color(0xffC5272D)
: Color(0xff2B353E),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 11, fontSize: 11,
letterSpacing: -0.33, letterSpacing: -0.33,
), ),
AppText( AppText(
TranslationBase.of(context).fileno + ": " + result.responseID.toString(), TranslationBase.of(context).fileno +
color: result.responseID == user!.patientID ? Color(0xffC5272D) : Color(0xff989898), ": " +
result.responseID.toString(),
color: result.responseID == user!.patientID
? Color(0xffC5272D)
: Color(0xff989898),
fontSize: 11, fontSize: 11,
letterSpacing: -0.33, letterSpacing: -0.33,
), ),
@ -348,8 +372,11 @@ class _AppDrawerState extends State<AppDrawer> {
), ),
mHeight(20), mHeight(20),
InkWell( InkWell(
child: DrawerItem(TranslationBase.of(context).arabicChange, child: DrawerItem(
Padding(child: Image.asset('assets/images/lang.png'), padding: EdgeInsets.only(left: 3, right: 3, top: 3, bottom: projectProvider!.isArabic ? 3 : 0)), TranslationBase.of(context).arabicChange,
Padding(
child: Image.asset('assets/images/lang.png'),
padding: EdgeInsets.only(left: 3, right: 3, top: 3, bottom: projectProvider!.isArabic ? 3 : 0)),
isImageIcon: true, isImageIcon: true,
bottomLine: false, bottomLine: false,
letterSpacing: -0.84, letterSpacing: -0.84,
@ -429,8 +456,13 @@ class _AppDrawerState extends State<AppDrawer> {
), ),
if (projectProvider!.havePrivilege(3)) if (projectProvider!.havePrivilege(3))
InkWell( InkWell(
child: DrawerItem(TranslationBase.of(context).appsetting, SvgPicture.asset("assets/images/new/app_setting.svg"), child: DrawerItem(
isImageIcon: true, bottomLine: false, letterSpacing: -0.84, fontSize: 14, projectProvider: projectProvider), TranslationBase.of(context).appsetting, SvgPicture.asset("assets/images/new/app_setting.svg"),
isImageIcon: true,
bottomLine: false,
letterSpacing: -0.84,
fontSize: 14,
projectProvider: projectProvider),
onTap: () { onTap: () {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
SETTINGS, SETTINGS,
@ -439,7 +471,8 @@ class _AppDrawerState extends State<AppDrawer> {
}, },
), ),
InkWell( InkWell(
child: DrawerItem(TranslationBase.of(context).rateApp, Icons.star, bottomLine: false, letterSpacing: -0.84, fontSize: 14, projectProvider: projectProvider), child: DrawerItem(TranslationBase.of(context).rateApp, Icons.star,
bottomLine: false, letterSpacing: -0.84, fontSize: 14, projectProvider: projectProvider),
onTap: () { onTap: () {
openAppReviewDialog(); openAppReviewDialog();
locator<GAnalytics>().hamburgerMenu.logMenuItemClick('rate our app'); locator<GAnalytics>().hamburgerMenu.logMenuItemClick('rate our app');
@ -452,7 +485,11 @@ class _AppDrawerState extends State<AppDrawer> {
), ),
InkWell( InkWell(
child: DrawerItem(TranslationBase.of(context).logout, SvgPicture.asset("assets/images/new/logout.svg"), child: DrawerItem(TranslationBase.of(context).logout, SvgPicture.asset("assets/images/new/logout.svg"),
isImageIcon: true, bottomLine: false, letterSpacing: -0.84, fontSize: 14, projectProvider: projectProvider), isImageIcon: true,
bottomLine: false,
letterSpacing: -0.84,
fontSize: 14,
projectProvider: projectProvider),
onTap: () { onTap: () {
logout(); logout();
}, },
@ -466,8 +503,46 @@ class _AppDrawerState extends State<AppDrawer> {
login(); login();
}, },
), ),
// Dev Tools
if (ENABLE_DEV_TOOLS) InkWell(
child: DrawerItem('API Logs', Icons.api_outlined,
letterSpacing: -0.84, fontSize: 14, bottomLine: false, projectProvider: projectProvider),
onTap: () {
Navigator.of(context).pop();
Future.delayed(const Duration(milliseconds: 300), () {
locator<NavigationService>()
.navigatorKey
.currentState!
.push(MaterialPageRoute(builder: (_) => const ApiLogScreen()));
});
},
),
if (ENABLE_DEV_TOOLS) InkWell(
child: DrawerItem('Environment Config', Icons.tune,
letterSpacing: -0.84, fontSize: 14, bottomLine: false, projectProvider: projectProvider),
onTap: () async {
Navigator.of(context).pop();
// Wait for drawer close animation before showing sheet
await Future.delayed(const Duration(milliseconds: 300));
final navState = locator<NavigationService>().navigatorKey.currentState;
if (navState == null) return;
final shouldRestart = await showModalBottomSheet<bool>(
context: navState.context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const EnvironmentConfigSheet(),
);
if (shouldRestart == true) {
// Delay to let the sheet fully dismiss before navigating
await Future.delayed(const Duration(milliseconds: 300));
locator<NavigationService>().navigatorKey.currentState!.pushNamedAndRemoveUntil(SPLASH, (route) => false);
}
},
),
//
InkWell( InkWell(
child: DrawerItem(TranslationBase.of(context).privacyPolicy, Icons.web, letterSpacing: -0.84, fontSize: 14, bottomLine: false), child: DrawerItem(TranslationBase.of(context).privacyPolicy, Icons.web,
letterSpacing: -0.84, fontSize: 14, bottomLine: false),
onTap: () { onTap: () {
if (projectProvider!.isArabic) if (projectProvider!.isArabic)
launch("https://hmg.com/ar/Pages/Privacy.aspx"); launch("https://hmg.com/ar/Pages/Privacy.aspx");
@ -476,7 +551,8 @@ class _AppDrawerState extends State<AppDrawer> {
}, },
), ),
InkWell( InkWell(
child: DrawerItem(TranslationBase.of(context).termsConditions, Icons.web, letterSpacing: -0.84, fontSize: 14, bottomLine: false), child: DrawerItem(TranslationBase.of(context).termsConditions, Icons.web,
letterSpacing: -0.84, fontSize: 14, bottomLine: false),
onTap: () { onTap: () {
Navigator.of(context).push(FadePage(page: UserAgreementPage())); Navigator.of(context).push(FadePage(page: UserAgreementPage()));
}, },
@ -653,8 +729,6 @@ class _AppDrawerState extends State<AppDrawer> {
builder: (BuildContext context) => SavedLogin(savedData), builder: (BuildContext context) => SavedLogin(savedData),
), ),
); );
} else { } else {
GifLoaderDialogUtils.showMyDialog(context); GifLoaderDialogUtils.showMyDialog(context);
authService.selectDeviceImei(DEVICE_TOKEN).then((value) async { authService.selectDeviceImei(DEVICE_TOKEN).then((value) async {
@ -735,15 +809,13 @@ class _AppDrawerState extends State<AppDrawer> {
GifLoaderDialogUtils.showMyDialog(context); GifLoaderDialogUtils.showMyDialog(context);
sharedPref.remove(BLOOD_TYPE); sharedPref.remove(BLOOD_TYPE);
this.familyFileProvider.silentLoggin(user is AuthenticatedUser ? null : user, languageID, mainUser: user is AuthenticatedUser).then((value) { this.familyFileProvider.silentLoggin(user is AuthenticatedUser ? null : user, languageID, mainUser: user is AuthenticatedUser).then((value) {
// GifLoaderDialogUtils.hideDialog(context);
// Navigator.of(context).pop();
loginAfter(value, context, user is AuthenticatedUser); loginAfter(value, context, user is AuthenticatedUser);
locator<GAnalytics>().hamburgerMenu.logMenuItemClick('switch to my family account'); locator<GAnalytics>().hamburgerMenu.logMenuItemClick('switch to my family account');
}).catchError((err) { }).catchError((err) {
GifLoaderDialogUtils.hideDialog(context); GifLoaderDialogUtils.hideDialog(context);
print(err); print(err);
// AppToast.showErrorToast(message: err.toString());
Navigator.of(context).pop(); Navigator.of(context).pop();
return;
}); });
} }
@ -852,8 +924,7 @@ class _AppDrawerState extends State<AppDrawer> {
}) })
.catchError((err) { .catchError((err) {
print(err); print(err);
//Utils.hideProgressDialog(); return <dynamic>{};
// GifLoaderDialogUtils.hideDialog(context);
}); });
} }

Loading…
Cancel
Save