diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md new file mode 100644 index 00000000..00ee9672 --- /dev/null +++ b/PROJECT_ARCHITECTURE.md @@ -0,0 +1,1768 @@ + +You are a senior Flutter developer. You will be given this entire document as your specification. +Your task: generate a complete, compilable Flutter project from scratch following every instruction below. +Do NOT skip any file. Do NOT summarize. Generate FULL, working Dart code for every file listed. +When you see placeholder names like `{{APP_NAME}}`, `{{PACKAGE_NAME}}`, `{{BASE_URL}}`, replace them with the values the user provides. If no values are provided, use the defaults in Section 1. + + +# Flutter Project Generator — Architecture Specification + +> **What this document is:** A precise, machine-readable specification for an AI to generate a greenfield Flutter project. Every file path, every class, every method signature, and every pattern is defined below with copy-ready code. Follow it top-to-bottom. + +--- + +## 1. DEFAULTS — Replace These for Your Project + +``` +APP_NAME = "My App" +DART_PACKAGE = "my_app" +ORG_IDENTIFIER = "com.example.myapp" +BASE_URL = "https://api.example.com/" +PRIMARY_COLOR = 0xFFED1C2B (Red) +FIGMA_WIDTH = 430 +FIGMA_HEIGHT = 927 +SUPPORTED_LOCALES = [en-US, ar-SA] +PRIMARY_FONT = "Poppins" +SECONDARY_FONT = "GESSTwo" (Arabic font) +``` + +When the user asks you to generate this project, first ask them for these values. If they say "use defaults," use the values above. + +--- + +## 2. COMMAND — Create the Flutter Project + +Run this first: +```bash +flutter create --org {{ORG_IDENTIFIER}} --project-name {{DART_PACKAGE}} {{DART_PACKAGE}} +cd {{DART_PACKAGE}} +``` + +--- + +## 3. FILE: `pubspec.yaml` + +Generate this file EXACTLY. Do not add or remove packages. + +```yaml +name: {{DART_PACKAGE}} +description: "{{APP_NAME}}" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ">=3.6.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + + # === Core Architecture === + provider: ^6.1.5+1 + get_it: ^8.2.0 + dartz: ^0.10.1 + equatable: ^2.0.7 + + # === Networking === + http: ^1.5.0 + connectivity_plus: ^6.1.5 + + # === Firebase === + firebase_core: any + firebase_messaging: ^15.2.10 + firebase_analytics: ^11.5.1 + firebase_crashlytics: ^4.3.8 + + # === UI === + cupertino_icons: ^1.0.8 + flutter_svg: ^2.2.0 + cached_network_image: ^3.4.1 + auto_size_text: ^3.0.0 + shimmer: ^3.0.0 + sizer: ^3.1.3 + lottie: ^3.3.1 + smooth_corner: ^1.1.1 + flutter_staggered_animations: ^1.1.1 + fl_chart: 1.0.0 + flutter_rating_bar: ^4.0.1 + + # === Localization === + easy_localization: ^3.0.8 + intl: ^0.20.2 + + # === Storage === + shared_preferences: ^2.5.3 + path_provider: ^2.0.8 + + # === Device === + permission_handler: ^12.0.1 + local_auth: ^2.3.0 + device_info_plus: ^11.5.0 + image_picker: ^1.2.0 + url_launcher: ^6.3.2 + share_plus: ^11.1.0 + + # === Notifications === + flutter_local_notifications: ^19.4.1 + timezone: ^0.10.0 + fluttertoast: ^8.2.12 + + # === Logging === + logger: ^2.6.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + assets: + - assets/ + - assets/json/ + - assets/fonts/ + - assets/langs/ + - assets/images/ + - assets/images/svg/ + - assets/images/png/ + - assets/animations/ + - assets/animations/lottie/ + + fonts: + - family: Poppins + fonts: + - asset: assets/fonts/poppins/Poppins-SemiBold.ttf + weight: 600 + - asset: assets/fonts/poppins/Poppins-Medium.ttf + weight: 500 + - asset: assets/fonts/poppins/Poppins-Regular.ttf + weight: 400 + - asset: assets/fonts/poppins/Poppins-Light.ttf + weight: 300 +``` + +> **Note:** If the user provides a SECONDARY_FONT, add its font family block here too. + +--- + +## 4. FILE: `analysis_options.yaml` + +```yaml +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # prefer_single_quotes: true +``` + +--- + +## 5. FOLDER STRUCTURE — Create ALL These Directories + +```bash +mkdir -p lib/core/api +mkdir -p lib/core/common_models +mkdir -p lib/core/exceptions +mkdir -p lib/core/utils +mkdir -p lib/services/analytics +mkdir -p lib/features +mkdir -p lib/presentation/home/widgets +mkdir -p lib/presentation/authentication +mkdir -p lib/presentation/onboarding +mkdir -p lib/routes +mkdir -p lib/theme +mkdir -p lib/extensions +mkdir -p lib/widgets/buttons +mkdir -p lib/widgets/loader +mkdir -p lib/widgets/bottomsheet +mkdir -p lib/widgets/bottom_navigation +mkdir -p lib/widgets/shimmer +mkdir -p lib/widgets/routes +mkdir -p lib/generated +mkdir -p assets/fonts/poppins +mkdir -p assets/images/svg +mkdir -p assets/images/png +mkdir -p assets/animations/lottie +mkdir -p assets/json +mkdir -p assets/langs +mkdir -p assets/sounds +``` + +--- + +## 6. CORE LAYER — Generate Each File Exactly + +### 6.1 FILE: `lib/core/enums.dart` + +```dart +enum ViewStateEnum { hide, idle, busy, error, busyLocal, errorLocal } + +enum AppEnvironmentTypeEnum { dev, uat, preProd, qa, staging, prod } + +enum GenderTypeEnum { male, female } + +enum ChipTypeEnum { success, error, alert, info, warning, lightBg, primaryRed } + +enum LoginTypeEnum { sms, whatsapp, face, fingerprint } + +enum OTPTypeEnum { sms, whatsapp, faceIDFingerprint } +``` + +### 6.2 FILE: `lib/core/exceptions/api_failure.dart` + +```dart +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable implements Exception { + final String message; + const Failure(this.message); +} + +class ServerFailure extends Failure { + final String url; + const ServerFailure(super.message, {this.url = ""}); + @override + List get props => [message]; +} + +class DataParsingFailure extends Failure { + const DataParsingFailure(super.message); + @override + List get props => [message]; +} + +class ConnectivityFailure extends Failure { + const ConnectivityFailure(super.message); + @override + List get props => [message]; +} + +class UnAuthenticatedUserFailure extends Failure { + final String url; + const UnAuthenticatedUserFailure(super.message, {this.url = ""}); + @override + List get props => [message]; +} + +class AppUpdateFailure extends Failure { + const AppUpdateFailure(super.message); + @override + List get props => [message]; +} + +class StatusCodeFailure extends Failure { + const StatusCodeFailure(super.message); + @override + List get props => [message]; +} + +class UnknownFailure extends Failure { + final String url; + const UnknownFailure(super.message, {this.url = ""}); + @override + List get props => [message]; +} + +class UserIntimationFailure extends Failure { + const UserIntimationFailure(super.message); + @override + List get props => [message]; +} + +class MessageStatusFailure extends Failure { + const MessageStatusFailure(super.message); + @override + List get props => [message]; +} + +class InvalidCredentials extends Failure { + const InvalidCredentials(String? message) : super(message ?? ''); + @override + List get props => [message]; +} + +class LocalStorageFailure extends Failure { + const LocalStorageFailure(super.message); + @override + List get props => [message]; +} +``` + +### 6.3 FILE: `lib/core/common_models/generic_api_model.dart` + +```dart +class GenericApiModel { + final int? messageStatus; + final String? errorMessage; + final int? statusCode; + final T? data; + + GenericApiModel({this.messageStatus, this.errorMessage, this.statusCode, this.data}); + + factory GenericApiModel.fromJson( + Map json, + T Function(Object? json)? fromJsonT, + ) { + return GenericApiModel( + messageStatus: json['messageStatus'] as int?, + errorMessage: json['errorMessage'] as String?, + statusCode: json['statusCode'] as int?, + data: fromJsonT != null ? fromJsonT(json['data']) : json['data'] as T?, + ); + } + + Map toJson(Object Function(T value)? toJsonT) { + return { + 'messageStatus': messageStatus, + 'errorMessage': errorMessage, + 'statusCode': statusCode, + 'data': toJsonT != null && data != null ? toJsonT(data as T) : data, + }; + } +} +``` + +### 6.4 FILE: `lib/core/api_consts.dart` + +```dart +import 'package:{{DART_PACKAGE}}/core/enums.dart'; + +const int VERSION_ID = 1; +const int CHANNEL = 3; +const String IP_ADDRESS = "10.20.10.20"; +const String GENERAL_ID = "Aborkan"; +const int PATIENT_TYPE = 1; +const int PATIENT_TYPE_ID = 1; +const String SETUP_ID = "010266"; +const bool IS_DENTAL_ALLOWED_BACKEND = false; + +class ApiConsts { + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static String baseUrl = '{{BASE_URL}}'; + + static int appVersionID = VERSION_ID; + static int appChannelId = CHANNEL; + static String appIpAddress = IP_ADDRESS; + static String appGeneralId = GENERAL_ID; + static String sessionID = ""; + + static void setBackendURLs() { + switch (appEnvironmentType) { + case AppEnvironmentTypeEnum.prod: + baseUrl = '{{BASE_URL}}'; + break; + case AppEnvironmentTypeEnum.dev: + case AppEnvironmentTypeEnum.uat: + case AppEnvironmentTypeEnum.preProd: + case AppEnvironmentTypeEnum.qa: + case AppEnvironmentTypeEnum.staging: + baseUrl = '{{BASE_URL}}'; + break; + } + } + + // ── Add endpoint paths here as you build features ── + // static const String login = "api/auth/login"; +} +``` + +### 6.5 FILE: `lib/core/cache_consts.dart` + +```dart +class CacheConst { + static const String firstLaunch = "firstLaunch"; + static const String appAuthToken = "app_auth_token"; + static const String loggedInUserObj = "logged_in_user_obj"; + static const String pushToken = "push_token"; + static const String appLanguage = 'language'; + static const String isDarkMode = 'is_dark_mode'; + + // Add more cache keys as features are built +} +``` + +### 6.6 FILE: `lib/core/app_state.dart` + +```dart +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; + +class AppState { + final NavigationService navigationService; + + AppState({required this.navigationService}); + + double userLat = 0.0; + double userLong = 0.0; + + bool isArabic() { + final ctx = navigationService.navigatorKey.currentContext; + if (ctx == null) return false; + return EasyLocalization.of(ctx)?.locale.languageCode == "ar"; + } + + int getLanguageID() => isArabic() ? 1 : 2; + String? getLanguageCode() { + final ctx = navigationService.navigatorKey.currentContext; + if (ctx == null) return "en"; + return EasyLocalization.of(ctx)?.locale.languageCode; + } + int getDeviceTypeID() => Platform.isIOS ? 1 : 2; + + bool isAuthenticated = false; + String appAuthToken = ""; + String deviceToken = ""; + String voipToken = ""; + String sessionId = ""; + String deviceTypeID = ""; + + Map? authenticatedUser; + + void setAuthenticatedUser(Map? user) { + authenticatedUser = user; + isAuthenticated = user != null; + } + + void resetLocation() { + userLat = 0.0; + userLong = 0.0; + } +} +``` + +### 6.7 FILE: `lib/core/app_assets.dart` + +```dart +class AppAssets { + static const String svgBasePath = 'assets/images/svg'; + static const String pngBasePath = 'assets/images/png'; + static const String lottieBasePath = 'assets/animations/lottie'; + + // Add asset constants here as you add files. Examples: + // static const String logo = '$svgBasePath/logo.svg'; + // static const String placeholder = '$pngBasePath/placeholder.png'; +} +``` + +### 6.8 FILE: `lib/core/app_export.dart` + +```dart +export '../routes/app_routes.dart'; +export 'utils/size_utils.dart'; +``` + +### 6.9 FILE: `lib/core/utils/size_utils.dart` + +```dart +import 'package:flutter/material.dart'; + +const num figmaDesignWidth = {{FIGMA_WIDTH}}; +const num figmaDesignHeight = {{FIGMA_HEIGHT}}; + +class SizeUtils { + static late double width; + static late double height; + static late DeviceType deviceType; + + static void init(BoxConstraints constraints, Orientation orientation) { + width = constraints.maxWidth; + height = constraints.maxHeight; + deviceType = width > 600 ? DeviceType.tablet : DeviceType.phone; + } +} + +enum DeviceType { phone, tablet } + +bool get isTablet => SizeUtils.deviceType == DeviceType.tablet; + +extension ResponsiveExtension on num { + double get _width => SizeUtils.width; + double get _height => SizeUtils.height; + + double get f { + double scale = (_width < _height ? _width : _height) / figmaDesignWidth; + double clamp = isTablet ? 1.4 : 1.2; + if (scale > clamp) scale = clamp; + return this * scale; + } + + double get w => (this * _width) / figmaDesignWidth; + double get h => (this * _height) / figmaDesignHeight; + double get r => (this * _width) / figmaDesignWidth; +} +``` + +### 6.10 FILE: `lib/core/utils/loading_utils.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; + +class LoadingUtils { + static bool isLoading = false; + static OverlayEntry? _overlayEntry; + + static void showFullScreenLoader() { + final context = getIt.get().navigatorKey.currentContext; + if (context == null || isLoading) return; + isLoading = true; + _overlayEntry = OverlayEntry( + builder: (_) => Container( + color: Colors.black38, + child: const Center(child: CircularProgressIndicator()), + ), + ); + Overlay.of(context).insert(_overlayEntry!); + } + + static void hideFullScreenLoader() { + _overlayEntry?.remove(); + _overlayEntry = null; + isLoading = false; + } +} +``` + +### 6.11 FILE: `lib/core/api/api_client.dart` + +```dart +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:{{DART_PACKAGE}}/core/api_consts.dart'; +import 'package:{{DART_PACKAGE}}/core/app_state.dart'; +import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; + +abstract class ApiClient { + Future post( + String endPoint, { + required Map body, + required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, + required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, + bool isExternal = false, + Map? apiHeaders, + }); + + Future get( + String endPoint, { + required Function(dynamic response, int statusCode) onSuccess, + required Function(String error, int statusCode) onFailure, + Map? queryParams, + Map? apiHeaders, + bool isExternal = false, + }); +} + +class ApiClientImp implements ApiClient { + final AppState _appState; + + ApiClientImp({required AppState appState}) : _appState = appState; + + Map _defaultHeaders() => {'Content-Type': 'application/json', 'Accept': 'application/json'}; + + void _injectCommonFields(Map body) { + body['VersionID'] = ApiConsts.appVersionID.toString(); + body['Channel'] = ApiConsts.appChannelId.toString(); + body['IPAdress'] = ApiConsts.appIpAddress; + body['generalid'] = ApiConsts.appGeneralId; + body['LanguageID'] = _appState.getLanguageID().toString(); + body['Latitude'] = _appState.userLat.toString(); + body['Longitude'] = _appState.userLong.toString(); + body['DeviceTypeID'] = _appState.deviceTypeID; + if (_appState.appAuthToken.isNotEmpty) { + body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; + } + } + + @override + Future post( + String endPoint, { + required Map body, + required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, + required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, + bool isExternal = false, + Map? apiHeaders, + }) async { + final url = isExternal ? endPoint : ApiConsts.baseUrl + endPoint; + final headers = apiHeaders ?? _defaultHeaders(); + if (!isExternal) _injectCommonFields(body); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: jsonEncode(body)); + final statusCode = response.statusCode; + + if (statusCode == 401) { + onFailure('Unauthenticated', statusCode, failureType: UnAuthenticatedUserFailure('Session expired', url: url)); + return; + } + if (statusCode == 426) { + onFailure('App update required', statusCode, failureType: const AppUpdateFailure('Please update the app')); + return; + } + + if (statusCode >= 200 && statusCode < 300) { + final decoded = jsonDecode(response.body); + final int? messageStatus = decoded['MessageStatus']; + final String? errorMessage = decoded['ErrorEndUserMessage'] ?? decoded['ErrorMessage']; + + if (messageStatus == 1 || messageStatus == null) { + onSuccess(decoded, statusCode, messageStatus: messageStatus, errorMessage: errorMessage); + } else { + onFailure(errorMessage ?? 'Request failed', statusCode, + messageStatus: messageStatus, + failureType: MessageStatusFailure(errorMessage ?? 'Request failed with status $messageStatus')); + } + } else { + onFailure('Server error: $statusCode', statusCode, failureType: ServerFailure('Server error: $statusCode', url: url)); + } + } catch (e) { + if (kDebugMode) print('API POST error ($url): $e'); + onFailure(e.toString(), 0, failureType: UnknownFailure(e.toString(), url: url)); + } + } + + @override + Future get( + String endPoint, { + required Function(dynamic response, int statusCode) onSuccess, + required Function(String error, int statusCode) onFailure, + Map? queryParams, + Map? apiHeaders, + bool isExternal = false, + }) async { + final baseUrl = isExternal ? endPoint : ApiConsts.baseUrl + endPoint; + final uri = Uri.parse(baseUrl).replace(queryParameters: queryParams?.map((k, v) => MapEntry(k, v.toString()))); + final headers = apiHeaders ?? _defaultHeaders(); + + try { + final response = await http.get(uri, headers: headers); + if (response.statusCode >= 200 && response.statusCode < 300) { + onSuccess(jsonDecode(response.body), response.statusCode); + } else { + onFailure('GET failed: ${response.statusCode}', response.statusCode); + } + } catch (e) { + onFailure(e.toString(), 0); + } + } +} +``` + +--- + +## 7. SERVICES LAYER — Generate Each File Exactly + +### 7.1 FILE: `lib/services/logger_service.dart` + +```dart +import 'package:logger/logger.dart'; + +abstract class LoggerService { + void logError(String message); + void logInfo(String message); +} + +class LoggerServiceImp implements LoggerService { + final Logger logger; + LoggerServiceImp({required this.logger}); + + @override + void logError(String message) => logger.e(message); + @override + void logInfo(String message) => logger.d(message); +} +``` + +### 7.2 FILE: `lib/services/navigation_service.dart` + +```dart +import 'package:flutter/material.dart'; + +class NavigationService { + final GlobalKey navigatorKey = GlobalKey(); + + BuildContext? get context => navigatorKey.currentContext; + + Future push(Route route) => navigatorKey.currentState!.push(route); + + Future pushAndRemoveUntil(Route route, RoutePredicate predicate) => + navigatorKey.currentState!.pushAndRemoveUntil(route, predicate); + + void pop([T? result]) => navigatorKey.currentState!.pop(result); + + void popUntilNamed(String routeName) => + navigatorKey.currentState?.popUntil(ModalRoute.withName(routeName)); + + void replaceAllRoutesAndNavigateTo(String routeName) { + navigatorKey.currentState?.pushNamedAndRemoveUntil(routeName, (route) => false); + } + + void pushAndReplace(String routeName) => + navigatorKey.currentState?.pushReplacementNamed(routeName); + + void pushNamed(String routeName, {Object? arguments}) => + navigatorKey.currentState?.pushNamed(routeName, arguments: arguments); + + Future pushPage({required Widget page, bool fullscreenDialog = false}) => + navigatorKey.currentState!.push( + MaterialPageRoute(builder: (_) => page, fullscreenDialog: fullscreenDialog), + ); +} +``` + +### 7.3 FILE: `lib/services/cache_service.dart` + +```dart +import 'dart:convert'; +import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class CacheService { + Future saveString({required String key, required String value}); + Future saveInt({required String key, required int value}); + Future saveBool({required String key, required bool value}); + String? getString({required String key}); + int? getInt({required String key}); + bool? getBool({required String key}); + Future getObject({required String key}); + Future saveObject({required String key, required dynamic value}); + Future remove({required String key}); + Future clear(); +} + +class CacheServiceImp implements CacheService { + final SharedPreferences sharedPreferences; + final LoggerService loggerService; + + CacheServiceImp({required this.sharedPreferences, required this.loggerService}); + + @override + Future saveString({required String key, required String value}) async => await sharedPreferences.setString(key, value); + @override + Future saveInt({required String key, required int value}) async => await sharedPreferences.setInt(key, value); + @override + Future saveBool({required String key, required bool value}) async => await sharedPreferences.setBool(key, value); + @override + String? getString({required String key}) => sharedPreferences.getString(key); + @override + int? getInt({required String key}) => sharedPreferences.getInt(key); + @override + bool? getBool({required String key}) => sharedPreferences.getBool(key); + + @override + Future getObject({required String key}) async { + try { + await sharedPreferences.reload(); + final s = sharedPreferences.getString(key); + return s == null ? null : json.decode(s); + } catch (e) { + loggerService.logError(e.toString()); + return null; + } + } + + @override + Future saveObject({required String key, required dynamic value}) async => + await sharedPreferences.setString(key, json.encode(value)); + @override + Future remove({required String key}) async => await sharedPreferences.remove(key); + @override + Future clear() async => await sharedPreferences.clear(); +} +``` + +### 7.4 FILE: `lib/services/dialog_service.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +abstract class DialogService { + Future showErrorBottomSheet({String title, required String message, Function()? onOkPressed}); + Future showConfirmBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); +} + +class DialogServiceImp implements DialogService { + final NavigationService navigationService; + bool _isSheetShowing = false; + + DialogServiceImp({required this.navigationService}); + + @override + Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed}) async { + if (_isSheetShowing) return; + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + _isSheetShowing = true; + await showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (_) => Padding( + padding: const EdgeInsets.all(24), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + if (title.isNotEmpty) ...[Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 12)], + Text(message, textAlign: TextAlign.center), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryRedColor), + onPressed: () { Navigator.pop(context); onOkPressed?.call(); }, + child: const Text('OK', style: TextStyle(color: Colors.white)), + ), + ), + ]), + ), + ); + _isSheetShowing = false; + } + + @override + Future showConfirmBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + await showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (_) => Padding( + padding: const EdgeInsets.all(24), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(message, textAlign: TextAlign.center), + const SizedBox(height: 24), + Row(children: [ + Expanded(child: OutlinedButton(onPressed: () { Navigator.pop(context); onCancelPressed?.call(); }, child: const Text('Cancel'))), + const SizedBox(width: 12), + Expanded(child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryRedColor), + onPressed: () { Navigator.pop(context); onOkPressed(); }, + child: const Text('OK', style: TextStyle(color: Colors.white)), + )), + ]), + ]), + ), + ); + } +} +``` + +### 7.5 FILE: `lib/services/error_handler_service.dart` + +```dart +import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; +import 'package:{{DART_PACKAGE}}/core/utils/loading_utils.dart'; +import 'package:{{DART_PACKAGE}}/services/dialog_service.dart'; +import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; +import 'package:{{DART_PACKAGE}}/routes/app_routes.dart'; + +abstract class ErrorHandlerService { + Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}); +} + +class ErrorHandlerServiceImp implements ErrorHandlerService { + final DialogService dialogService; + final LoggerService loggerService; + final NavigationService navigationService; + + ErrorHandlerServiceImp({required this.dialogService, required this.loggerService, required this.navigationService}); + + @override + Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}) async { + if (LoadingUtils.isLoading) LoadingUtils.hideFullScreenLoader(); + + if (failure is ServerFailure) { + loggerService.logError("Server: ${failure.message}"); + await _show(failure); + } else if (failure is ConnectivityFailure) { + loggerService.logError("Connectivity: ${failure.message}"); + await _show(failure, title: "No Internet"); + } else if (failure is UnAuthenticatedUserFailure) { + loggerService.logError("Unauth: ${failure.message}"); + await _show(failure, title: "Session Expired", onOk: () => navigationService.replaceAllRoutesAndNavigateTo(AppRoutes.landingScreen)); + } else if (failure is AppUpdateFailure) { + await _show(failure, title: "Update Required"); + } else if (failure is DataParsingFailure) { + loggerService.logError("Parse: ${failure.message}"); + await _show(failure, title: "Data Error"); + } else if (failure is UserIntimationFailure) { + onUnHandledFailure != null ? onUnHandledFailure(failure) : await _show(failure, onOk: onOkPressed); + } else if (failure is MessageStatusFailure) { + onMessageStatusFailure != null ? onMessageStatusFailure(failure) : await _show(failure, onOk: onOkPressed); + } else { + loggerService.logError("Unhandled: $failure"); + await _show(failure, onOk: onOkPressed); + } + } + + Future _show(Failure f, {String? title, Function()? onOk}) async => + await dialogService.showErrorBottomSheet(title: title ?? "", message: f.message, onOkPressed: onOk); +} +``` + +### 7.6 FILE: `lib/services/firebase_service.dart` + +```dart +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:{{DART_PACKAGE}}/core/app_state.dart'; +import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; + +abstract class FirebaseService { + Future getDeviceToken(); +} + +class FirebaseServiceImpl implements FirebaseService { + final FirebaseMessaging firebaseMessaging; + final LoggerService loggerService; + final AppState appState; + + FirebaseServiceImpl({required this.firebaseMessaging, required this.loggerService, required this.appState}); + + @override + Future getDeviceToken() async { + try { + final token = await firebaseMessaging.getToken(); + appState.deviceToken = token ?? ""; + return appState.deviceToken; + } catch (e) { + loggerService.logError(e.toString()); + return ""; + } + } +} +``` + +### 7.7 FILE: `lib/services/notification_service.dart` + +```dart +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; +import 'package:timezone/data/latest_all.dart' as tz; + +abstract class NotificationService { + Future initialize({Function(String payload)? onNotificationClick}); + Future showNotification({required String title, required String body, String? payload}); + Future cancelAllNotifications(); +} + +class NotificationServiceImp implements NotificationService { + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + final LoggerService loggerService; + + NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService}); + + @override + Future initialize({Function(String payload)? onNotificationClick}) async { + tz.initializeTimeZones(); + const android = AndroidInitializationSettings('app_icon'); + const ios = DarwinInitializationSettings(requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true); + await flutterLocalNotificationsPlugin.initialize( + const InitializationSettings(android: android, iOS: ios), + onDidReceiveNotificationResponse: (r) => onNotificationClick?.call(r.payload ?? ''), + ); + } + + @override + Future showNotification({required String title, required String body, String? payload}) async { + const details = NotificationDetails( + android: AndroidNotificationDetails('general', 'General', importance: Importance.high, priority: Priority.high), + iOS: DarwinNotificationDetails(), + ); + await flutterLocalNotificationsPlugin.show(DateTime.now().millisecondsSinceEpoch ~/ 1000, title, body, details, payload: payload); + } + + @override + Future cancelAllNotifications() async => await flutterLocalNotificationsPlugin.cancelAll(); +} +``` + +### 7.8 FILE: `lib/services/analytics/analytics_service.dart` + +```dart +import 'package:firebase_analytics/firebase_analytics.dart'; + +class GAnalytics { + final _analytics = FirebaseAnalytics.instance; + + Future logEvent(String name, {Map? parameters}) async { + try { + await _analytics.logEvent(name: name.trim().toLowerCase(), parameters: parameters?.map((k, v) => MapEntry(k, v as Object))); + } catch (_) {} + } + + Future setUser({String? userId, String? language}) async { + if (userId != null) _analytics.setUserProperty(name: 'userid', value: userId); + if (language != null) _analytics.setUserProperty(name: 'user_language', value: language); + } +} +``` + +--- + +## 8. THEME LAYER + +### 8.1 FILE: `lib/theme/colors.dart` + +```dart +import 'package:flutter/material.dart'; + +class AppColors { + static bool isDarkMode = false; + static const transparent = Colors.transparent; + + static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8); + static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFFFFFFF); + static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : const Color({{PRIMARY_COLOR}}); + static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); + static const Color successColor = Color(0xFF18C273); + static Color get textColor => isDarkMode ? dark.textColor : const Color(0xFF2E3039); + static Color get iconColor => isDarkMode ? dark.iconColor : const Color(0xFF2E3039); + static Color get greyColor => isDarkMode ? dark.greyColor : const Color(0xFFEFEFF0); + static Color get textColorLight => isDarkMode ? dark.textColorLight : const Color(0xFF5E5E5E); + static Color get dividerColor => isDarkMode ? dark.dividerColor : const Color(0x40D2D2D2); + static Color get borderGrayColor => isDarkMode ? dark.borderGrayColor : const Color(0x332E3039); + static Color get greyTextColor => isDarkMode ? dark.greyTextColor : const Color(0xFF8F9AA3); + static Color get inputLabelTextColor => isDarkMode ? dark.inputLabelTextColor : const Color(0xFF898A8D); + static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); + static Color get warningColor => isDarkMode ? dark.warningColor : const Color(0xFFFFCC00); + static Color get infoColor => isDarkMode ? dark.infoColor : const Color(0xFF0B85F7); + static Color get shimmerBaseColor => isDarkMode ? dark.shimmerBaseColor : const Color(0xFFE0E0E0); + static Color get shimmerHighlightColor => isDarkMode ? dark.shimmerHighlightColor : const Color(0xFFF5F5F5); + + static Color get blackColor => textColor; + static Color get bgScaffoldColor => scaffoldBgColor; + + static const AppColorsDark dark = AppColorsDark(); +} + +class AppColorsDark { + const AppColorsDark(); + Color get scaffoldBgColor => const Color(0xFF191919); + Color get whiteColor => const Color(0xFF1E1E1E); + Color get primaryRedColor => const Color(0xFFDE5C5D); + Color get secondaryLightRedColor => const Color(0xFF3A1015); + Color get textColor => const Color(0xFFFFFFFF); + Color get iconColor => const Color(0xFFFFFFFF); + Color get greyColor => const Color(0xFF2C2C2E); + Color get textColorLight => const Color(0xFFB0B0B0); + Color get dividerColor => const Color(0x33FFFFFF); + Color get borderGrayColor => const Color(0x55ECECEC); + Color get greyTextColor => const Color(0xFF8F9AA3); + Color get inputLabelTextColor => const Color(0xFF9E9E9E); + Color get errorColor => const Color(0xFFD63D48); + Color get warningColor => const Color(0xFFFFCC00); + Color get infoColor => const Color(0xFF4DA6FF); + Color get shimmerBaseColor => const Color(0xFF2C2C2E); + Color get shimmerHighlightColor => const Color(0xFF3A3A3C); +} +``` + +### 8.2 FILE: `lib/theme/app_theme.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +class AppTheme { + static ThemeData getTheme(bool isArabic) => ThemeData( + fontFamily: isArabic ? '{{SECONDARY_FONT}}' : '{{PRIMARY_FONT}}', + primarySwatch: Colors.red, + brightness: Brightness.light, + visualDensity: VisualDensity.adaptivePlatformDensity, + pageTransitionsTheme: const PageTransitionsTheme(builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }), + canvasColor: Colors.white, + splashColor: Colors.transparent, + appBarTheme: const AppBarTheme(elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.light, surfaceTintColor: Colors.transparent), + ); + + static ThemeData getDarkTheme(bool isArabic) => ThemeData( + fontFamily: isArabic ? '{{SECONDARY_FONT}}' : '{{PRIMARY_FONT}}', + primarySwatch: Colors.red, + brightness: Brightness.dark, + visualDensity: VisualDensity.adaptivePlatformDensity, + pageTransitionsTheme: const PageTransitionsTheme(builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }), + canvasColor: AppColors.dark.scaffoldBgColor, + scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, + splashColor: Colors.transparent, + appBarTheme: AppBarTheme(color: AppColors.dark.scaffoldBgColor, elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.light, surfaceTintColor: Colors.transparent), + ); +} +``` + +--- + +## 9. EXTENSIONS + +### 9.1 FILE: `lib/extensions/int_extensions.dart` + +```dart +import 'package:flutter/material.dart'; + +extension IntExtensions on int { + Widget get height => SizedBox(height: toDouble()); + Widget get width => SizedBox(width: toDouble()); + Widget get divider => Divider(height: toDouble(), thickness: toDouble(), color: const Color(0x30D2D2D2)); +} +``` + +### 9.2 FILE: `lib/extensions/string_extensions.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/core/utils/size_utils.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +extension CapExtension on String { + String get inCaps => isEmpty ? '' : '${this[0].toUpperCase()}${substring(1)}'; + String get capitalizeFirstOfEach => trim().isEmpty ? '' : trim().toLowerCase().split(' ').map((s) => s.inCaps).join(' '); +} + +extension StringToWidget on String { + Widget _text(double size, {Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, bool isUnderLine = false, int? maxLines, TextOverflow? overflow, double letterSpacing = 0}) { + return Text(this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: maxLines, + overflow: overflow, + style: TextStyle(fontSize: size.f, fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: letterSpacing, decoration: isUnderLine ? TextDecoration.underline : null, decorationColor: color ?? AppColors.blackColor), + ); + } + + Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxLines, TextOverflow? overflow}) => _text(8, color: color, fontWeight: fontWeight, isBold: isBold, maxLines: maxLines, overflow: overflow); + Widget toText10({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(10, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); + Widget toText11({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines}) => _text(11, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines); + Widget toText12({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(12, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); + Widget toText13({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(13, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); + Widget toText14({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(14, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); + Widget toText16({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(16, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); + Widget toText18({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines}) => _text(18, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines); + Widget toText20({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(20, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); + Widget toText24({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(24, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); + Widget toText30({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(30, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); +} +``` + +### 9.3 FILE: `lib/extensions/widget_extensions.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +extension WidgetExtensions on Widget { + Widget onPress(VoidCallback onTap) => InkWell(onTap: onTap, child: this); + Widget get expanded => Expanded(child: this); + Widget get center => Center(child: this); + Widget circle(double v) => ClipRRect(borderRadius: BorderRadius.circular(v), child: this); + Widget paddingAll(double v) => Padding(padding: EdgeInsets.all(v), child: this); + Widget paddingSymmetrical(double h, double v) => Padding(padding: EdgeInsets.symmetric(horizontal: h, vertical: v), child: this); + Widget paddingOnly({double left = 0, double right = 0, double top = 0, double bottom = 0}) => Padding(padding: EdgeInsetsDirectional.only(start: left, end: right, top: top, bottom: bottom), child: this); + Widget toShimmer({bool isShow = true}) => isShow ? Shimmer.fromColors(baseColor: AppColors.shimmerBaseColor, highlightColor: AppColors.shimmerHighlightColor, child: this) : this; + Widget objectContainerView({double radius = 15}) => Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(radius), boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.15), blurRadius: 26, offset: const Offset(0, -3))]), + child: this, + ); +} +``` + +### 9.4 FILE: `lib/extensions/context_extensions.dart` + +```dart +import 'package:flutter/material.dart'; + +extension ContextUtils on BuildContext { + double get screenHeight => MediaQuery.of(this).size.height; + double get screenWidth => MediaQuery.of(this).size.width; + ThemeData get theme => Theme.of(this); + TextTheme get textTheme => theme.textTheme; +} + +extension ShowBottomSheetExt on BuildContext { + Future showSheet({required Widget child, bool isScrollControlled = true, bool isDismissible = true}) { + return showModalBottomSheet(context: this, isScrollControlled: isScrollControlled, isDismissible: isDismissible, backgroundColor: Colors.transparent, builder: (_) => child); + } +} +``` + +### 9.5 FILE: `lib/extensions/route_extensions.dart` + +```dart +import 'package:flutter/material.dart'; + +extension NavigationExtensions on BuildContext { + void navigateWithName(String routeName, {Object? arguments}) => Navigator.pushNamed(this, routeName, arguments: arguments); + Future navigateReplaceWithName(String routeName, {Object? arguments}) async => await Navigator.pushReplacementNamed(this, routeName, arguments: arguments); + void navigateAndRemoveAll(String routeName) => Navigator.pushNamedAndRemoveUntil(this, routeName, (route) => false); + void pop() => Navigator.of(this).pop(); + void pushTo(Widget page) => Navigator.push(this, MaterialPageRoute(builder: (_) => page)); +} +``` + +--- + +## 10. WIDGETS — Starter Widgets + +### 10.1 FILE: `lib/widgets/buttons/custom_button.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/core/utils/size_utils.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +class CustomButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final Color? color; + final Color? textColor; + final double? width; + final bool isOutlined; + final bool isLoading; + + const CustomButton({super.key, required this.label, this.onPressed, this.color, this.textColor, this.width, this.isOutlined = false, this.isLoading = false}); + + @override + Widget build(BuildContext context) { + final bg = color ?? AppColors.primaryRedColor; + return SizedBox( + width: width ?? double.infinity, + height: 48.h, + child: isOutlined + ? OutlinedButton(onPressed: isLoading ? null : onPressed, style: OutlinedButton.styleFrom(side: BorderSide(color: bg), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))), child: _child(bg)) + : ElevatedButton(onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom(backgroundColor: bg, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))), child: _child(textColor ?? Colors.white)), + ); + } + + Widget _child(Color c) => isLoading ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2, color: c)) : Text(label, style: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, color: c)); +} +``` + +### 10.2 FILE: `lib/widgets/routes/fade_page.dart` + +```dart +import 'package:flutter/material.dart'; + +class FadePage extends PageRouteBuilder { + final Widget page; + FadePage({required this.page}) : super( + pageBuilder: (_, __, ___) => page, + transitionsBuilder: (_, a, __, child) => FadeTransition(opacity: a, child: child), + transitionDuration: const Duration(milliseconds: 300), + ); +} +``` + +--- + +## 11. DEPENDENCY INJECTION — `lib/core/dependencies.dart` + +```dart +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logger/logger.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:{{DART_PACKAGE}}/core/api/api_client.dart'; +import 'package:{{DART_PACKAGE}}/core/app_state.dart'; +import 'package:{{DART_PACKAGE}}/services/analytics/analytics_service.dart'; +import 'package:{{DART_PACKAGE}}/services/cache_service.dart'; +import 'package:{{DART_PACKAGE}}/services/dialog_service.dart'; +import 'package:{{DART_PACKAGE}}/services/error_handler_service.dart'; +import 'package:{{DART_PACKAGE}}/services/firebase_service.dart'; +import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; +import 'package:{{DART_PACKAGE}}/services/notification_service.dart'; + +final GetIt getIt = GetIt.instance; + +class AppDependencies { + static Future addDependencies() async { + final logger = Logger(printer: PrettyPrinter(methodCount: 2, errorMethodCount: 5, lineLength: 1000, colors: true, printEmojis: true)); + + getIt.registerLazySingleton(() => LoggerServiceImp(logger: logger)); + getIt.registerLazySingleton(() => NavigationService()); + getIt.registerLazySingleton(() => AppState(navigationService: getIt())); + getIt.registerLazySingleton(() => GAnalytics()); + getIt.registerLazySingleton(() => DialogServiceImp(navigationService: getIt())); + getIt.registerLazySingleton(() => ErrorHandlerServiceImp(dialogService: getIt(), loggerService: getIt(), navigationService: getIt())); + + final sp = await SharedPreferences.getInstance(); + getIt.registerLazySingleton(() => CacheServiceImp(sharedPreferences: sp, loggerService: getIt())); + getIt.registerLazySingleton(() => ApiClientImp(appState: getIt())); + getIt.registerLazySingleton(() => FirebaseServiceImpl(firebaseMessaging: FirebaseMessaging.instance, loggerService: getIt(), appState: getIt())); + + final flnp = FlutterLocalNotificationsPlugin(); + getIt.registerLazySingleton(() => NotificationServiceImp(flutterLocalNotificationsPlugin: flnp, loggerService: getIt())); + + // ═══ Register feature repos & ViewModels below as you add them ═══ + } +} +``` + +--- + +## 12. ROUTING — `lib/routes/app_routes.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/presentation/home/navigation_screen.dart'; +import 'package:{{DART_PACKAGE}}/splash_page.dart'; + +class AppRoutes { + static const String initialRoute = '/'; + static const String landingScreen = '/landingScreen'; + + // Add route names as you build features: + // static const String loginScreen = '/loginScreen'; + + static Map get routes => { + initialRoute: (_) => const SplashPage(), + landingScreen: (_) => const LandingNavigation(), + }; +} +``` + +--- + +## 13. LOCALIZATION FILES + +### FILE: `assets/langs/en-US.json` + +```json +{ + "appName": "{{APP_NAME}}", + "home": "Home", + "services": "Services", + "medicalFile": "Medical File", + "bookAppointment": "Book Appointment", + "login": "Login", + "register": "Register", + "cancel": "Cancel", + "confirm": "Confirm", + "done": "Done", + "ok": "OK", + "loading": "Loading...", + "noDataAvailable": "No data available", + "noInternetConnection": "No internet connection", + "settings": "Settings", + "language": "Language", + "darkMode": "Dark Mode", + "logout": "Logout", + "search": "Search" +} +``` + +### FILE: `assets/langs/ar-SA.json` + +```json +{ + "appName": "{{APP_NAME}}", + "home": "الرئيسية", + "services": "الخدمات", + "medicalFile": "الملف الطبي", + "bookAppointment": "حجز موعد", + "login": "تسجيل الدخول", + "register": "التسجيل", + "cancel": "إلغاء", + "confirm": "تأكيد", + "done": "تم", + "ok": "حسنا", + "loading": "جاري التحميل...", + "noDataAvailable": "لا توجد بيانات", + "noInternetConnection": "لا يوجد اتصال بالإنترنت", + "settings": "الإعدادات", + "language": "اللغة", + "darkMode": "الوضع الداكن", + "logout": "تسجيل الخروج", + "search": "بحث" +} +``` + +--- + +## 14. PRESENTATION — Starter Screens + +### FILE: `lib/splash_page.dart` + +```dart +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/core/api_consts.dart'; +import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; +import 'package:{{DART_PACKAGE}}/presentation/home/navigation_screen.dart'; +import 'package:{{DART_PACKAGE}}/services/notification_service.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; +import 'package:{{DART_PACKAGE}}/widgets/routes/fade_page.dart'; + +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State { + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + ApiConsts.setBackendURLs(); + await getIt.get().initialize(); + Timer(const Duration(seconds: 2), () { + Navigator.of(context).pushReplacement(FadePage(page: const LandingNavigation())); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.whiteColor, + body: Center(child: Text('{{APP_NAME}}', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: AppColors.primaryRedColor))), + ); + } +} +``` + +### FILE: `lib/presentation/home/navigation_screen.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/presentation/home/landing_page.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +class LandingNavigation extends StatefulWidget { + const LandingNavigation({super.key}); + @override + State createState() => _LandingNavigationState(); +} + +class _LandingNavigationState extends State { + int _idx = 0; + final _pc = PageController(); + + final _pages = const [ + LandingPage(), + Center(child: Text('Medical File')), + Center(child: Text('Book Appointment')), + Center(child: Text('Services')), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: PageView(controller: _pc, physics: const NeverScrollableScrollPhysics(), children: _pages), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _idx, + type: BottomNavigationBarType.fixed, + selectedItemColor: AppColors.primaryRedColor, + unselectedItemColor: AppColors.greyTextColor, + onTap: (i) { setState(() => _idx = i); _pc.jumpToPage(i); }, + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.folder_outlined), activeIcon: Icon(Icons.folder), label: 'File'), + BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Book'), + BottomNavigationBarItem(icon: Icon(Icons.grid_view_outlined), activeIcon: Icon(Icons.grid_view), label: 'Services'), + ], + ), + ); + } +} +``` + +### FILE: `lib/presentation/home/landing_page.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/extensions/int_extensions.dart'; +import 'package:{{DART_PACKAGE}}/extensions/string_extensions.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +class LandingPage extends StatelessWidget { + const LandingPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + 24.height, + 'Welcome'.toText24(isBold: true), + 8.height, + 'What would you like to do today?'.toText14(color: AppColors.greyTextColor), + 24.height, + Expanded(child: Center(child: 'Home content goes here'.toText16(color: AppColors.textColorLight, isCenter: true))), + ]), + ), + ), + ); + } +} +``` + +--- + +## 15. ENTRY POINT — `lib/main.dart` + +```dart +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; +import 'package:{{DART_PACKAGE}}/routes/app_routes.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; +import 'package:{{DART_PACKAGE}}/theme/app_theme.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +class MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) => + super.createHttpClient(context)..badCertificateCallback = (cert, host, port) => true; +} + +Future _init() async { + WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); + // Uncomment when Firebase is configured: + // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await AppDependencies.addDependencies(); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + HttpOverrides.global = MyHttpOverrides(); +} + +void main() async { + await _init(); + runApp( + EasyLocalization( + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'SA')], + path: 'assets/langs', + fallbackLocale: const Locale('en', 'US'), + child: MultiProvider( + providers: [ + // ═══ Add ChangeNotifierProviders as you build features ═══ + ], + child: const MyApp(), + ), + ), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: !Platform.isIOS, + child: LayoutBuilder(builder: (context, constraints) { + return Sizer(builder: (context, orientation, deviceType) { + final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; + return MaterialApp( + title: '{{APP_NAME}}', + debugShowCheckedModeBanner: false, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + initialRoute: AppRoutes.initialRoute, + routes: AppRoutes.routes, + theme: AppTheme.getTheme(isArabic), + darkTheme: AppTheme.getDarkTheme(isArabic), + themeMode: AppColors.isDarkMode ? ThemeMode.dark : ThemeMode.light, + navigatorKey: getIt.get().navigatorKey, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling, alwaysUse24HourFormat: true), + child: child!, + ), + ); + }); + }), + ); + } +} +``` + +--- + +## 16. FEATURE MODULE TEMPLATE + +When asked to **add a new feature**, create these four files. Replace `{{feature}}` (snake_case) and `{{Feature}}` (PascalCase). + +### `lib/features/{{feature}}/{{feature}}_repo.dart` + +```dart +import 'package:dartz/dartz.dart'; +import 'package:{{DART_PACKAGE}}/core/api/api_client.dart'; +import 'package:{{DART_PACKAGE}}/core/common_models/generic_api_model.dart'; +import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; +import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; + +abstract class {{Feature}}Repo { + // Future>>> getItems(); +} + +class {{Feature}}RepoImp implements {{Feature}}Repo { + final ApiClient apiClient; + final LoggerService loggerService; + {{Feature}}RepoImp({required this.loggerService, required this.apiClient}); + + // @override + // Future>>> getItems() async { + // Map body = {}; + // try { + // GenericApiModel>? result; + // Failure? failure; + // await apiClient.post('endpoint', body: body, + // onFailure: (e, s, {messageStatus, failureType}) => failure = failureType, + // onSuccess: (r, s, {messageStatus, errorMessage}) { + // try { result = GenericApiModel(data: (r['Key'] as List).map((e) => YourModel.fromJson(e)).toList()); } + // catch (e) { failure = DataParsingFailure(e.toString()); } + // }); + // if (failure != null) return Left(failure!); + // return Right(result!); + // } catch (e) { return Left(UnknownFailure(e.toString())); } + // } +} +``` + +### `lib/features/{{feature}}/{{feature}}_view_model.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:{{DART_PACKAGE}}/features/{{feature}}/{{feature}}_repo.dart'; +import 'package:{{DART_PACKAGE}}/services/error_handler_service.dart'; +import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; + +class {{Feature}}ViewModel extends ChangeNotifier { + final {{Feature}}Repo _repo; + final ErrorHandlerService _errorHandler; + final NavigationService _nav; + + {{Feature}}ViewModel({required {{Feature}}Repo repo, required ErrorHandlerService errorHandler, required NavigationService nav}) + : _repo = repo, _errorHandler = errorHandler, _nav = nav; + + bool isLoading = false; + + // void init() { isLoading = true; notifyListeners(); _fetch(); } + // void _fetch() async { + // final r = await _repo.getItems(); + // r.fold((f) { isLoading = false; _errorHandler.handleError(failure: f); notifyListeners(); }, + // (d) { /* items = d.data ?? []; */ isLoading = false; notifyListeners(); }); + // } +} +``` + +### `lib/presentation/{{feature}}/{{feature}}_page.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:{{DART_PACKAGE}}/features/{{feature}}/{{feature}}_view_model.dart'; +import 'package:{{DART_PACKAGE}}/theme/colors.dart'; + +class {{Feature}}Page extends StatefulWidget { + const {{Feature}}Page({super.key}); + @override + State<{{Feature}}Page> createState() => _{{Feature}}PageState(); +} + +class _{{Feature}}PageState extends State<{{Feature}}Page> { + @override + void initState() { + super.initState(); + // Provider.of<{{Feature}}ViewModel>(context, listen: false).init(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: AppBar(title: const Text('{{Feature}}')), + body: Consumer<{{Feature}}ViewModel>( + builder: (_, vm, __) { + if (vm.isLoading) return const Center(child: CircularProgressIndicator()); + return const Center(child: Text('Content here')); + }, + ), + ); + } +} +``` + +### After creating, REGISTER: + +**`dependencies.dart`:** +```dart +getIt.registerLazySingleton<{{Feature}}Repo>(() => {{Feature}}RepoImp(loggerService: getIt(), apiClient: getIt())); +getIt.registerLazySingleton<{{Feature}}ViewModel>(() => {{Feature}}ViewModel(repo: getIt(), errorHandler: getIt(), nav: getIt())); +``` + +**`main.dart` providers list:** +```dart +ChangeNotifierProvider<{{Feature}}ViewModel>(create: (_) => getIt.get<{{Feature}}ViewModel>()), +``` + +**`app_routes.dart`:** +```dart +static const String {{feature}}Page = '/{{feature}}'; +// in routes map: {{feature}}Page: (_) => const {{Feature}}Page(), +``` + +--- + +## 17. RULES — ALWAYS FOLLOW + +1. **Every service/repo = abstract class + implementation.** Never use concrete directly. +2. **All DI in `dependencies.dart`.** Only use `getIt.get()` elsewhere. +3. **All repos return `Future>>`.** +4. **All ViewModels extend `ChangeNotifier`.** Call `notifyListeners()` after state changes. +5. **ViewModels NEVER import widgets.** Use `NavigationService` for navigation. +6. **Pages use `Consumer` or `Provider.of` to read ViewModel state.** +7. **Colors from `AppColors`** — never hardcode hex in widgets. +8. **Text via string extensions** — `'Hello'.toText16()` not `Text('Hello')`. +9. **Spacing via int extensions** — `16.height` not `SizedBox(height: 16)`. +10. **Sizes via responsive extensions** — `16.f`, `16.w`, `16.h`, `16.r`. +11. **Assets via `AppAssets`** — never hardcode paths. +12. **Cache keys in `CacheConst`** — never raw strings for SharedPreferences. +13. **Endpoints in `ApiConsts`** — never hardcode URLs. +14. **Errors through `ErrorHandlerService`** — VMs never show dialogs. +15. **User-facing strings use `.tr()`** from easy_localization. + +--- + +## 18. CHECKLIST — Verify Before Delivering + +- [ ] `flutter pub get` succeeds +- [ ] All imports use `package:{{DART_PACKAGE}}/...` +- [ ] Every file in this spec exists +- [ ] `dependencies.dart` registers all existing services/repos/VMs +- [ ] `main.dart` providers list includes all registered VMs +- [ ] `app_routes.dart` includes all existing pages +- [ ] `pubspec.yaml` declares all existing asset directories +- [ ] Both `en-US.json` and `ar-SA.json` exist +- [ ] No circular imports +- [ ] `AppColors` used everywhere (no raw `Color()` in widgets) +- [ ] Extensions used for text, spacing, widget wrapping + +--- + +*End of specification. Generate all files above. When asked to add a feature, use Section 16.* + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f1a0ed0c..71d30372 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -199,8 +199,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - body['TokenID'] = "@dm!n"; - body['PatientID'] = 2007395; + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 2007395; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 07cbbde3..4c689ab7 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -106,6 +106,11 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + closeAILabResultAnalysis() { + labOrderResponseByAi = null; + notifyListeners(); + } + void setIsSortByClinic(bool value) { isSortByClinic = value; patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 549b8b0d..9fb6ee1d 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -1,4 +1,3 @@ - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -101,18 +100,12 @@ class DoctorProfilePage extends StatelessWidget { children: [ Column( children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.doctor_profile_rating_icon, - width: 48.w, - height: 48.h, - fit: BoxFit.contain, - applyThemeColor: false - ), + Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_rating_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), "Ratings".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate .toString() - .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor), + .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { bookAppointmentsViewModel.getDoctorRatingDetails(); @@ -128,18 +121,12 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(width: 36.w), Column( children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.doctor_profile_reviews_icon, - width: 48.w, - height: 48.h, - fit: BoxFit.contain, - applyThemeColor: false - ), + Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_reviews_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), "Reviews".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate .toString() - .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor), + .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { bookAppointmentsViewModel.getDoctorRatingDetails(); diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart index af6c97f8..c883c863 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart @@ -164,7 +164,7 @@ class LabResultByClinic extends StatelessWidget { padding: EdgeInsets.only(right: 4.w, left: 4.w), child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), ), - LocaleKeys.generateAiAnalysis.tr(context: context).toText16(isBold: true) + LocaleKeys.generateAiAnalysis.tr(context: context).toText16(isBold: true, color: Colors.white) ], ), ).paddingSymmetrical(24.h, 24.h).onPress(() async { diff --git a/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart b/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart index 7a477761..2c552bc7 100644 --- a/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart +++ b/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart @@ -2,11 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_order_response_by_ai_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; class AiAnalysisWidget extends StatelessWidget { final LabOrderResponseByAi data; @@ -71,6 +74,14 @@ class AiAnalysisWidget extends StatelessWidget { ), ], ), + SizedBox(height: 16.h), + CustomButton( + height: 50.h, + text: LocaleKeys.close.tr(context: context), + onPressed: () { + getIt.get().closeAILabResultAnalysis(); + }, + ), ], ), ), diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 8d5d0042..69254f69 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -35,97 +35,99 @@ class LabResultDetails extends StatelessWidget { LabViewModel labViewModel = Provider.of(context, listen: false); final appState = getIt.get(); return Scaffold( - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: LocaleKeys.labResultDetails.tr(context: context), - // aiOverview: () async { - // final _dialogService = getIt.get(); - // await _dialogService.showCommonBottomSheetWithoutH( - // message: LocaleKeys.aiDisclaimer.tr(), - // label: LocaleKeys.consent.tr(), - // okLabel: LocaleKeys.acceptLbl.tr(), - // cancelLabel: LocaleKeys.rejectView.tr(), - // onOkPressed: () { - // context.pop(); - // labViewModel.getAiOverviewSingleLabResult(langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); - // }, - // onCancelPressed: () { - // context.pop(); - // }); - // }, - child: SingleChildScrollView( - child: Column( - spacing: 16.h, - children: [ - LabNameAndStatus(context), - getLabDescription(context), - LabGraph(context), - Selector( - selector: (_, model) => model.labOrderResponseByAi, - builder: (_, aiData, __) { - if (aiData != null) { - return AiAnalysisWidget(data: aiData).paddingOnly(bottom: 16.h); - } - return const SizedBox.shrink(); - }, - ), - ], - ).paddingAll(24.h), + body: Consumer(builder: (context, labVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.labResultDetails.tr(context: context), + // aiOverview: () async { + // final _dialogService = getIt.get(); + // await _dialogService.showCommonBottomSheetWithoutH( + // message: LocaleKeys.aiDisclaimer.tr(), + // label: LocaleKeys.consent.tr(), + // okLabel: LocaleKeys.acceptLbl.tr(), + // cancelLabel: LocaleKeys.rejectView.tr(), + // onOkPressed: () { + // context.pop(); + // labViewModel.getAiOverviewSingleLabResult(langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); + // }, + // onCancelPressed: () { + // context.pop(); + // }); + // }, + child: SingleChildScrollView( + child: Column( + spacing: 16.h, + children: [ + LabNameAndStatus(context), + getLabDescription(context), + LabGraph(context), + Selector( + selector: (_, model) => model.labOrderResponseByAi, + builder: (_, aiData, __) { + if (aiData != null) { + return AiAnalysisWidget(data: aiData).paddingOnly(bottom: 16.h); + } + return const SizedBox.shrink(); + }, + ), + ], + ).paddingAll(24.h), + ), ), ), - ), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: true, - ), - child: Container( - height: 56.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.r), - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - stops: [0.236, 1.0], // 53.6% and 100% - colors: [ - Color(0xFF8A38F5), // Transparent - Color(0xFFE20BBB), // Solid #F8F8F8 - ], - ), + labVM.labOrderResponseByAi == null ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(right: 4.w, left: 4.w), - child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + child: Container( + height: 56.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.236, 1.0], // 53.6% and 100% + colors: [ + Color(0xFF8A38F5), // Transparent + Color(0xFFE20BBB), // Solid #F8F8F8 + ], ), - LocaleKeys.generateAiAnalysisResult.tr(context: context).toText16(isBold: true) - ], - ), - ).paddingSymmetrical(24.h, 24.h).onPress(() async { - final _dialogService = getIt.get(); - await _dialogService.showCommonBottomSheetWithoutH( - message: LocaleKeys.aiDisclaimer.tr(), - label: LocaleKeys.consent.tr(), - okLabel: LocaleKeys.acceptLbl.tr(), - cancelLabel: LocaleKeys.rejectView.tr(), - onOkPressed: () { - context.pop(); - labViewModel.getAiOverviewSingleLabResult( - langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); - }, - onCancelPressed: () { - context.pop(); - }); - }), - ), - ], - ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(right: 4.w, left: 4.w), + child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + ), + LocaleKeys.generateAiAnalysisResult.tr(context: context).toText16(isBold: true, color: Colors.white) + ], + ), + ).paddingSymmetrical(24.h, 24.h).onPress(() async { + final _dialogService = getIt.get(); + await _dialogService.showCommonBottomSheetWithoutH( + message: LocaleKeys.aiDisclaimer.tr(), + label: LocaleKeys.consent.tr(), + okLabel: LocaleKeys.acceptLbl.tr(), + cancelLabel: LocaleKeys.rejectView.tr(), + onOkPressed: () { + context.pop(); + labViewModel.getAiOverviewSingleLabResult( + langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); + }, + onCancelPressed: () { + context.pop(); + }); + }), + ) : SizedBox.shrink(), + ], + ); + }), ); }