diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..e77609dc --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +ENV_NAME=development +BASE_URL=https://syncrow-dev.azurewebsites.net \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..4e9dcb81 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +ENV_NAME=production +BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file diff --git a/.env.staging b/.env.staging new file mode 100644 index 00000000..9565b426 --- /dev/null +++ b/.env.staging @@ -0,0 +1,2 @@ +ENV_NAME=staging +BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file diff --git a/.github/workflows/azure-static-web-apps-zealous-mushroom-0d31a3303.yml b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml similarity index 88% rename from .github/workflows/azure-static-web-apps-zealous-mushroom-0d31a3303.yml rename to .github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml index b8d576b9..e28d1bb2 100644 --- a/.github/workflows/azure-static-web-apps-zealous-mushroom-0d31a3303.yml +++ b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml @@ -31,13 +31,13 @@ jobs: run: flutter pub get - name: Build Flutter Web App - run: flutter build web + run: flutter build web --release --dart-define=FLAVOR=development - name: Build And Deploy id: builddeploy uses: Azure/static-web-apps-deploy@v1 with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ZEALOUS_MUSHROOM_0D31A3303 }} + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_POLITE_SMOKE_017C65C10 }} repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) action: "upload" ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### @@ -56,5 +56,5 @@ jobs: id: closepullrequest uses: Azure/static-web-apps-deploy@v1 with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ZEALOUS_MUSHROOM_0D31A3303 }} - action: "close" + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_POLITE_SMOKE_017C65C10 }} + action: "close" \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index e4d616c1..b6f83bdc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,25 +1,61 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", "configurations": [ + { - "name": "web", - "request": "launch", - "type": "dart" - }, - { - "name": "web (profile mode)", + + "name": "DEVELOPMENT", + "request": "launch", + "type": "dart", - "flutterMode": "profile" - }, - { - "name": "web (release mode)", + + "args": [ + + "--dart-define", + + "FLAVOR=development" + + ], + + "flutterMode": "debug" + + },{ + + "name": "STAGING", + "request": "launch", + "type": "dart", - "flutterMode": "release" - } + + "args": [ + + "--dart-define", + + "FLAVOR=staging" + + ], + + "flutterMode": "debug" + + },{ + + "name": "PRODUCTION", + + "request": "launch", + + "type": "dart", + + "args": [ + + "--dart-define", + + "FLAVOR=production" + + ], + + "flutterMode": "debug" + + }, + ] } \ No newline at end of file diff --git a/README.md b/README.md index 301f90fd..745fe6f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # syncrow_web -A new Flutter project. - ## Getting Started This project is a starting point for a Flutter application. @@ -14,3 +12,11 @@ A few resources to get you started if this is your first Flutter project: For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference. + + +## USEFUL COMMANDS + +Run on chrome: flutter run -d chrome --dart-define=FLAVOR='ENV_NAME' + +Build: flutter build web --release --dart-define=FLAVOR='ENV_NAME' + diff --git a/assets/icons/1gang.svg b/assets/icons/1gang.svg new file mode 100644 index 00000000..647dc350 --- /dev/null +++ b/assets/icons/1gang.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/2gang.svg b/assets/icons/2gang.svg new file mode 100644 index 00000000..6cfe191b --- /dev/null +++ b/assets/icons/2gang.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/ac_lock.svg b/assets/icons/ac_lock.svg new file mode 100644 index 00000000..9402fe6b --- /dev/null +++ b/assets/icons/ac_lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ac_schedule.svg b/assets/icons/ac_schedule.svg new file mode 100644 index 00000000..eb2394c9 --- /dev/null +++ b/assets/icons/ac_schedule.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/account_setting.svg b/assets/icons/account_setting.svg new file mode 100644 index 00000000..0b27b849 --- /dev/null +++ b/assets/icons/account_setting.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/automation_records.svg b/assets/icons/automation_records.svg new file mode 100644 index 00000000..2f5a1038 --- /dev/null +++ b/assets/icons/automation_records.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/bathroom.svg b/assets/icons/bathroom.svg index 51fc8b6a..8a75f646 100644 --- a/assets/icons/bathroom.svg +++ b/assets/icons/bathroom.svg @@ -1,29 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/bedroom.svg b/assets/icons/bedroom.svg index d579b003..6797009c 100644 --- a/assets/icons/bedroom.svg +++ b/assets/icons/bedroom.svg @@ -1,34 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - + + + + + + diff --git a/assets/icons/closed_door.svg b/assets/icons/closed_door.svg new file mode 100644 index 00000000..9cbf40dc --- /dev/null +++ b/assets/icons/closed_door.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/door_delay.svg b/assets/icons/door_delay.svg new file mode 100644 index 00000000..49dbbaef --- /dev/null +++ b/assets/icons/door_delay.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/dyi.svg b/assets/icons/dyi.svg index 7da61e8e..938d2ba2 100644 --- a/assets/icons/dyi.svg +++ b/assets/icons/dyi.svg @@ -1,14 +1,13 @@ - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/assets/icons/empty_records.svg b/assets/icons/empty_records.svg new file mode 100644 index 00000000..662a3e47 --- /dev/null +++ b/assets/icons/empty_records.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/factory_reset.svg b/assets/icons/factory_reset.svg index 7a47f24b..b7297165 100644 --- a/assets/icons/factory_reset.svg +++ b/assets/icons/factory_reset.svg @@ -1,10 +1,4 @@ - - - - - - - - - + + + diff --git a/assets/icons/firmware.svg b/assets/icons/firmware.svg new file mode 100644 index 00000000..d636fd10 --- /dev/null +++ b/assets/icons/firmware.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/frequency_icon.svg b/assets/icons/frequency_icon.svg new file mode 100644 index 00000000..d093af37 --- /dev/null +++ b/assets/icons/frequency_icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/logo-grey.svg b/assets/icons/logo-grey.svg new file mode 100644 index 00000000..4f835d2d --- /dev/null +++ b/assets/icons/logo-grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/main_door.svg b/assets/icons/main_door.svg new file mode 100644 index 00000000..5f378012 --- /dev/null +++ b/assets/icons/main_door.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/main_door_notifi.svg b/assets/icons/main_door_notifi.svg new file mode 100644 index 00000000..34d44e1c --- /dev/null +++ b/assets/icons/main_door_notifi.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/main_door_reports.svg b/assets/icons/main_door_reports.svg new file mode 100644 index 00000000..f0eb413a --- /dev/null +++ b/assets/icons/main_door_reports.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/office.svg b/assets/icons/office.svg index 03a2badd..479352c6 100644 --- a/assets/icons/office.svg +++ b/assets/icons/office.svg @@ -1,40 +1,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/open_close_door.svg b/assets/icons/open_close_door.svg new file mode 100644 index 00000000..d5aacdef --- /dev/null +++ b/assets/icons/open_close_door.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/open_close_records.svg b/assets/icons/open_close_records.svg new file mode 100644 index 00000000..9c5c585c --- /dev/null +++ b/assets/icons/open_close_records.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/opened_door.svg b/assets/icons/opened_door.svg new file mode 100644 index 00000000..386a66f1 --- /dev/null +++ b/assets/icons/opened_door.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/parlour.svg b/assets/icons/parlour.svg index 3298393a..52562dd2 100644 --- a/assets/icons/parlour.svg +++ b/assets/icons/parlour.svg @@ -1,30 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/power_active_icon.svg b/assets/icons/power_active_icon.svg new file mode 100644 index 00000000..28b1412a --- /dev/null +++ b/assets/icons/power_active_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/preferences.svg b/assets/icons/preferences.svg new file mode 100644 index 00000000..0a7a7fc0 --- /dev/null +++ b/assets/icons/preferences.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/records.svg b/assets/icons/records.svg new file mode 100644 index 00000000..9e316afd --- /dev/null +++ b/assets/icons/records.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg new file mode 100644 index 00000000..c626454d --- /dev/null +++ b/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/sign_out.svg b/assets/icons/sign_out.svg new file mode 100644 index 00000000..5980d13e --- /dev/null +++ b/assets/icons/sign_out.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/sos.svg b/assets/icons/sos.svg new file mode 100644 index 00000000..887e9f19 --- /dev/null +++ b/assets/icons/sos.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/sos_normal.svg b/assets/icons/sos_normal.svg new file mode 100644 index 00000000..ea795024 --- /dev/null +++ b/assets/icons/sos_normal.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/speedo_meter.svg b/assets/icons/speedo_meter.svg new file mode 100644 index 00000000..be3b5c4b --- /dev/null +++ b/assets/icons/speedo_meter.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/unlock_ic.svg b/assets/icons/unlock_ic.svg new file mode 100644 index 00000000..8ce3cc4d --- /dev/null +++ b/assets/icons/unlock_ic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/volt-meter.svg b/assets/icons/volt-meter.svg new file mode 100644 index 00000000..6691a7dd --- /dev/null +++ b/assets/icons/volt-meter.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/volt_meter_icon.svg b/assets/icons/volt_meter_icon.svg new file mode 100644 index 00000000..97b9037d --- /dev/null +++ b/assets/icons/volt_meter_icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/voltage_icon.svg b/assets/icons/voltage_icon.svg new file mode 100644 index 00000000..29b06678 --- /dev/null +++ b/assets/icons/voltage_icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/water_heater.svg b/assets/icons/water_heater.svg new file mode 100644 index 00000000..aa58e3fc --- /dev/null +++ b/assets/icons/water_heater.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/water_leak_detected.svg b/assets/icons/water_leak_detected.svg new file mode 100644 index 00000000..3d6f13b0 --- /dev/null +++ b/assets/icons/water_leak_detected.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/water_leak_normal.svg b/assets/icons/water_leak_normal.svg new file mode 100644 index 00000000..f1165c07 --- /dev/null +++ b/assets/icons/water_leak_normal.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/curtain.svg b/assets/images/curtain.svg new file mode 100644 index 00000000..34fb387a --- /dev/null +++ b/assets/images/curtain.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/scheduling.svg b/assets/images/scheduling.svg new file mode 100644 index 00000000..3492a0ea --- /dev/null +++ b/assets/images/scheduling.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/core/extension/build_context_x.dart b/lib/core/extension/build_context_x.dart deleted file mode 100644 index 50bc5972..00000000 --- a/lib/core/extension/build_context_x.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -extension BuildContextExt on BuildContext { - ThemeData get theme => Theme.of(this); - - TextTheme get textTheme => Theme.of(this).textTheme; - - AppBarTheme get appBarTheme => Theme.of(this).appBarTheme; - - Size get screenSize => MediaQuery.of(this).size; - - double get screenWidth => MediaQuery.of(this).size.width; - - double get screenHeight => MediaQuery.of(this).size.height; - - double get textScale => MediaQuery.textScalerOf(this).scale(1); -} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart deleted file mode 100644 index 8b137891..00000000 --- a/lib/core/theme/app_theme.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/main.dart b/lib/main.dart index 1bd86775..c544f227 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,46 +1,58 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/utils/app_routes.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; +import 'package:syncrow_web/utils/theme/theme.dart'; Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - initialSetup(); - String checkToken = await AuthBloc.getTokenAndValidate(); - GoRouter router = GoRouter( - initialLocation: checkToken == 'Success' ? RoutesConst.home : RoutesConst.auth, - routes: AppRoutes.getRoutes(), - ); - runApp(MyApp( - router: router, - )); + try { + const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development'); + await dotenv.load(fileName: '.env.$environment'); + WidgetsFlutterBinding.ensureInitialized(); + initialSetup(); + } catch (_) {} + runApp(MyApp()); } class MyApp extends StatelessWidget { - final GoRouter router; - const MyApp({ + MyApp({ super.key, - required this.router, }); + final GoRouter _router = GoRouter( + initialLocation: RoutesConst.auth, + routes: AppRoutes.getRoutes(), + redirect: (context, state) async { + String checkToken = await AuthBloc.getTokenAndValidate(); + final loggedIn = checkToken == 'Success'; + final goingToLogin = state.uri.toString() == RoutesConst.auth; + + if (!loggedIn && !goingToLogin) return RoutesConst.auth; + if (loggedIn && goingToLogin) return RoutesConst.home; + + return null; + }, + ); + @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => HomeBloc()), + BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), ) ], child: MaterialApp.router( - debugShowCheckedModeBanner: false, // Hide debug banner + debugShowCheckedModeBanner: false, scrollBehavior: const MaterialScrollBehavior().copyWith( dragDevices: { PointerDeviceKind.mouse, @@ -49,27 +61,8 @@ class MyApp extends StatelessWidget { PointerDeviceKind.unknown, }, ), - - theme: ThemeData( - fontFamily: 'Aftika', - textTheme: const TextTheme( - bodySmall: TextStyle( - fontSize: 13, color: ColorsManager.whiteColors, fontWeight: FontWeight.bold), - bodyMedium: TextStyle(color: Colors.black87, fontSize: 14), - bodyLarge: TextStyle(fontSize: 16, color: Colors.white), - headlineSmall: TextStyle(color: Colors.black87, fontSize: 18), - headlineMedium: TextStyle(color: Colors.black87, fontSize: 20), - titleMedium: TextStyle(color: Colors.black87, fontSize: 22, fontWeight: FontWeight.bold ), - headlineLarge: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - routeInformationProvider: router.routeInformationProvider, - routerDelegate: router.routerDelegate, - routeInformationParser: router.routeInformationParser, + theme: myTheme, + routerConfig: _router, )); } } diff --git a/lib/pages/access_management/bloc/access_bloc.dart b/lib/pages/access_management/bloc/access_bloc.dart index 473d19bd..3e74dbff 100644 --- a/lib/pages/access_management/bloc/access_bloc.dart +++ b/lib/pages/access_management/bloc/access_bloc.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/common/hour_picker_dialog.dart'; import 'package:syncrow_web/services/access_mang_api.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/app_enum.dart'; @@ -11,22 +12,23 @@ import 'package:syncrow_web/utils/snack_bar.dart'; class AccessBloc extends Bloc { AccessBloc() : super((AccessInitial())) { on(_onFetchTableData); - // on(selectFilterTap); on(selectTime); on(_filterData); on(resetSearch); on(onTabChanged); } + String startTime = 'Start Date'; String endTime = 'End Date'; - int? effectiveTimeTimeStamp; int? expirationTimeTimeStamp; TextEditingController passwordName = TextEditingController(); + TextEditingController emailAuthorizer = TextEditingController(); List filteredData = []; List data = []; - Future _onFetchTableData(FetchTableData event, Emitter emit) async { + Future _onFetchTableData( + FetchTableData event, Emitter emit) async { try { emit(AccessLoaded()); data = await AccessMangApi().fetchVisitorPassword(); @@ -39,19 +41,28 @@ class AccessBloc extends Bloc { } void updateTabsCount() { - int toBeEffectiveCount = - data.where((item) => item.passwordStatus.value == 'To be effective').length; - int effectiveCount = data.where((item) => item.passwordStatus.value == 'Effective').length; - int expiredCount = data.where((item) => item.passwordStatus.value == 'Expired').length; + int toBeEffectiveCount = data + .where((item) => item.passwordStatus.value == 'To be effective') + .length; + int effectiveCount = + data.where((item) => item.passwordStatus.value == 'Effective').length; + int expiredCount = + data.where((item) => item.passwordStatus.value == 'Expired').length; tabs[1] = 'To Be Effective ($toBeEffectiveCount)'; tabs[2] = 'Effective ($effectiveCount)'; tabs[3] = 'Expired ($expiredCount)'; } int selectedIndex = 0; - final List tabs = ['All', 'To Be Effective (0)', 'Effective (0)', 'Expired']; + final List tabs = [ + 'All', + 'To Be Effective (0)', + 'Effective (0)', + 'Expired' + ]; - Future selectFilterTap(TabChangedEvent event, Emitter emit) async { + Future selectFilterTap( + TabChangedEvent event, Emitter emit) async { try { emit(AccessLoaded()); selectedIndex = event.selectedIndex; @@ -63,115 +74,176 @@ class AccessBloc extends Bloc { } } - Future selectTime(SelectTime event, Emitter emit) async { + Future selectTime( + SelectTime event, + Emitter emit, + ) async { emit(AccessLoaded()); final DateTime? picked = await showDatePicker( context: event.context, initialDate: DateTime.now(), - firstDate: DateTime(2015, 8), - lastDate: DateTime(2101), + firstDate: DateTime.now().add(const Duration(days: -5095)), + lastDate: DateTime.now().add(const Duration(days: 2095)), + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: ColorScheme.light( + primary: ColorsManager.blackColor, + onPrimary: Colors.white, + onSurface: ColorsManager.grayColor, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.blue, + ), + ), + ), + child: child!, + ); + }, ); if (picked != null) { - final selectedDateTime = DateTime( - picked.year, - picked.month, - picked.day, + final TimeOfDay? timePicked = await showHourPicker( + context: event.context, + initialTime: TimeOfDay.now(), ); - final selectedTimestamp = DateTime( - selectedDateTime.year, - selectedDateTime.month, - selectedDateTime.day, - selectedDateTime.hour, - selectedDateTime.minute, - ).millisecondsSinceEpoch ~/ - 1000; // Divide by 1000 to remove milliseconds - if (event.isStart) { - if (expirationTimeTimeStamp != null && selectedTimestamp > expirationTimeTimeStamp!) { - CustomSnackBar.displaySnackBar('Effective Time cannot be later than Expiration Time.'); + + if (timePicked != null) { + final DateTime selectedDateTime = DateTime( + picked.year, + picked.month, + picked.day, + timePicked.hour, + timePicked.minute, + ); + final int selectedTimestamp = + selectedDateTime.millisecondsSinceEpoch ~/ 1000; + if (event.isStart) { + if (expirationTimeTimeStamp != null && + selectedTimestamp > expirationTimeTimeStamp!) { + CustomSnackBar.displaySnackBar( + 'Effective Time cannot be later than Expiration Time.'); + } else { + startTime = selectedDateTime.toString().split('.').first; + effectiveTimeTimeStamp = selectedTimestamp; + } } else { - startTime = - selectedDateTime.toString().split('.').first; // Remove seconds and milliseconds - effectiveTimeTimeStamp = selectedTimestamp; - } - } else { - if (effectiveTimeTimeStamp != null && selectedTimestamp < effectiveTimeTimeStamp!) { - CustomSnackBar.displaySnackBar('Expiration Time cannot be earlier than Effective Time.'); - } else { - endTime = selectedDateTime.toString().split('.').first; // Remove seconds and milliseconds - expirationTimeTimeStamp = selectedTimestamp; + if (effectiveTimeTimeStamp != null && + selectedTimestamp < effectiveTimeTimeStamp!) { + CustomSnackBar.displaySnackBar( + 'Expiration Time cannot be earlier than Effective Time.'); + } else { + endTime = selectedDateTime.toString().split('.').first; + expirationTimeTimeStamp = selectedTimestamp; + } } } } emit(ChangeTimeState()); } - Future _filterData(FilterDataEvent event, Emitter emit) async { + Future _filterData( + FilterDataEvent event, Emitter emit) async { emit(AccessLoaded()); try { + // Convert search text to lower case for case-insensitive search + final searchText = event.passwordName?.toLowerCase() ?? ''; + final searchEmailText = event.emailAuthorizer?.toLowerCase() ?? ''; filteredData = data.where((item) { bool matchesCriteria = true; - // Convert timestamp to DateTime and extract date component - DateTime effectiveDate = - DateTime.fromMillisecondsSinceEpoch(int.parse(item.effectiveTime.toString()) * 1000) - .toUtc() - .toLocal(); - DateTime invalidDate = - DateTime.fromMillisecondsSinceEpoch(int.parse(item.invalidTime.toString()) * 1000) - .toUtc() - .toLocal(); - DateTime effectiveDateOnly = - DateTime(effectiveDate.year, effectiveDate.month, effectiveDate.day); - DateTime invalidDateOnly = DateTime(invalidDate.year, invalidDate.month, invalidDate.day); + DateTime effectiveDate = DateTime.fromMillisecondsSinceEpoch( + int.parse(item.effectiveTime.toString()) * 1000) + .toUtc() + .toLocal(); + DateTime invalidDate = DateTime.fromMillisecondsSinceEpoch( + int.parse(item.invalidTime.toString()) * 1000) + .toUtc() + .toLocal(); + DateTime effectiveDateAndTime = DateTime( + effectiveDate.year, + effectiveDate.month, + effectiveDate.day, + effectiveDate.hour, + effectiveDate.minute); + DateTime invalidDateAndTime = DateTime( + invalidDate.year, + invalidDate.month, + invalidDate.day, + invalidDate.hour, + invalidDate.minute); - // Filter by password name - if (event.passwordName != null && event.passwordName!.isNotEmpty) { + // Filter by password name, making the search case-insensitive + if (searchText.isNotEmpty) { final bool matchesName = - item.passwordName != null && item.passwordName.contains(event.passwordName); + item.passwordName.toString().toLowerCase().contains(searchText); if (!matchesName) { matchesCriteria = false; } } - - // Filter by start date only - if (event.startTime != null && event.endTime == null) { - DateTime startDateOnly = - DateTime.fromMillisecondsSinceEpoch(event.startTime! * 1000).toUtc().toLocal(); - startDateOnly = DateTime(startDateOnly.year, startDateOnly.month, startDateOnly.day); - if (effectiveDateOnly.isBefore(startDateOnly)) { + if (searchEmailText.isNotEmpty) { + final bool matchesName = item.authorizerEmail + .toString() + .toLowerCase() + .contains(searchEmailText); + if (!matchesName) { + matchesCriteria = false; + } + } + // Filter by start date only + if (event.startTime != null && event.endTime == null) { + DateTime startDateTime = + DateTime.fromMillisecondsSinceEpoch(event.startTime! * 1000) + .toUtc() + .toLocal(); + startDateTime = DateTime(startDateTime.year, startDateTime.month, + startDateTime.day, startDateTime.hour, startDateTime.minute); + if (effectiveDateAndTime.isBefore(startDateTime)) { matchesCriteria = false; } } - // Filter by end date only if (event.endTime != null && event.startTime == null) { - DateTime endDateOnly = - DateTime.fromMillisecondsSinceEpoch(event.endTime! * 1000).toUtc().toLocal(); - endDateOnly = DateTime(endDateOnly.year, endDateOnly.month, endDateOnly.day); - if (invalidDateOnly.isAfter(endDateOnly)) { + DateTime startDateTime = + DateTime.fromMillisecondsSinceEpoch(event.endTime! * 1000) + .toUtc() + .toLocal(); + startDateTime = DateTime(startDateTime.year, startDateTime.month, + startDateTime.day, startDateTime.hour, startDateTime.minute); + if (invalidDateAndTime.isAfter(startDateTime)) { matchesCriteria = false; } } // Filter by both start date and end date if (event.startTime != null && event.endTime != null) { - DateTime startDateOnly = - DateTime.fromMillisecondsSinceEpoch(event.startTime! * 1000).toUtc().toLocal(); - DateTime endDateOnly = - DateTime.fromMillisecondsSinceEpoch(event.endTime! * 1000).toUtc().toLocal(); - startDateOnly = DateTime(startDateOnly.year, startDateOnly.month, startDateOnly.day); - endDateOnly = DateTime(endDateOnly.year, endDateOnly.month, endDateOnly.day); - if (effectiveDateOnly.isBefore(startDateOnly) || invalidDateOnly.isAfter(endDateOnly)) { + DateTime startDateTime = + DateTime.fromMillisecondsSinceEpoch(event.startTime! * 1000) + .toUtc() + .toLocal(); + DateTime endDateTime = + DateTime.fromMillisecondsSinceEpoch(event.endTime! * 1000) + .toUtc() + .toLocal(); + startDateTime = DateTime(startDateTime.year, startDateTime.month, + startDateTime.day, startDateTime.hour, startDateTime.minute); + endDateTime = DateTime(endDateTime.year, endDateTime.month, + endDateTime.day, endDateTime.hour, endDateTime.minute); + if (effectiveDateAndTime.isBefore(startDateTime) || + invalidDateAndTime.isAfter(endDateTime)) { matchesCriteria = false; } } // Filter by selected tab index - if (event.selectedTabIndex == 1 && item.passwordStatus.value != 'To be effective') { + if (event.selectedTabIndex == 1 && + item.passwordStatus.value != 'To be effective') { matchesCriteria = false; - } else if (event.selectedTabIndex == 2 && item.passwordStatus.value != 'Effective') { + } else if (event.selectedTabIndex == 2 && + item.passwordStatus.value != 'Effective') { matchesCriteria = false; - } else if (event.selectedTabIndex == 3 && item.passwordStatus.value != 'Expired') { + } else if (event.selectedTabIndex == 3 && + item.passwordStatus.value != 'Expired') { matchesCriteria = false; } @@ -189,6 +261,7 @@ class AccessBloc extends Bloc { startTime = 'Start Time'; endTime = 'End Time'; passwordName.clear(); + emailAuthorizer.clear(); selectedIndex = 0; effectiveTimeTimeStamp = null; expirationTimeTimeStamp = null; @@ -196,11 +269,14 @@ class AccessBloc extends Bloc { } String timestampToDate(dynamic timestamp) { - DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp) * 1000); - return "${dateTime.year}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.day.toString().padLeft(2, '0')}"; + DateTime dateTime = + DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp) * 1000); + return "${dateTime.year}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.day.toString().padLeft(2, '0')} " + " ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; } - Future onTabChanged(TabChangedEvent event, Emitter emit) async { + Future onTabChanged( + TabChangedEvent event, Emitter emit) async { try { emit(AccessLoaded()); selectedIndex = event.selectedIndex; @@ -209,14 +285,19 @@ class AccessBloc extends Bloc { filteredData = data; break; case 1: // To Be Effective - filteredData = - data.where((item) => item.passwordStatus.value == "To Be Effective").toList(); + filteredData = data + .where((item) => item.passwordStatus.value == "To Be Effective") + .toList(); break; case 2: // Effective - filteredData = data.where((item) => item.passwordStatus.value == "Effective").toList(); + filteredData = data + .where((item) => item.passwordStatus.value == "Effective") + .toList(); break; case 3: // Expired - filteredData = data.where((item) => item.passwordStatus.value == "Expired").toList(); + filteredData = data + .where((item) => item.passwordStatus.value == "Expired") + .toList(); break; default: filteredData = data; @@ -224,6 +305,7 @@ class AccessBloc extends Bloc { add(FilterDataEvent( selectedTabIndex: selectedIndex, passwordName: passwordName.text.toLowerCase(), + emailAuthorizer: emailAuthorizer.text.toLowerCase(), startTime: effectiveTimeTimeStamp, endTime: expirationTimeTimeStamp)); emit(TableLoaded(filteredData)); diff --git a/lib/pages/access_management/bloc/access_event.dart b/lib/pages/access_management/bloc/access_event.dart index 1bd7dbd3..4ef71ea6 100644 --- a/lib/pages/access_management/bloc/access_event.dart +++ b/lib/pages/access_management/bloc/access_event.dart @@ -28,12 +28,14 @@ class SelectTime extends AccessEvent { class FilterDataEvent extends AccessEvent { final String? passwordName; + final String? emailAuthorizer; final int? startTime; final int? endTime; final int selectedTabIndex; // Add this field const FilterDataEvent({ this.passwordName, + this.emailAuthorizer, this.startTime, this.endTime, required this.selectedTabIndex, // Initialize this field diff --git a/lib/pages/access_management/model/password_model.dart b/lib/pages/access_management/model/password_model.dart index 8436ef56..0ce4426a 100644 --- a/lib/pages/access_management/model/password_model.dart +++ b/lib/pages/access_management/model/password_model.dart @@ -6,10 +6,13 @@ class PasswordModel { final dynamic effectiveTime; final dynamic passwordCreated; final dynamic createdTime; - final dynamic passwordName; // New field + final dynamic passwordName; final AccessStatus passwordStatus; final AccessType passwordType; final dynamic deviceUuid; + final dynamic authorizerEmail; + final dynamic authorizerDate; + final dynamic deviceName; PasswordModel({ this.passwordId, @@ -17,10 +20,13 @@ class PasswordModel { this.effectiveTime, this.passwordCreated, this.createdTime, - this.passwordName, // New field + this.passwordName, required this.passwordStatus, required this.passwordType, this.deviceUuid, + this.authorizerEmail, + this.authorizerDate, + this.deviceName, }); factory PasswordModel.fromJson(Map json) { @@ -30,10 +36,13 @@ class PasswordModel { effectiveTime: json['effectiveTime'], passwordCreated: json['passwordCreated'], createdTime: json['createdTime'], - passwordName: json['passwordName'] ?? 'No name', // New field + passwordName: json['passwordName']??'No Name', passwordStatus: AccessStatusExtension.fromString(json['passwordStatus']), passwordType: AccessTypeExtension.fromString(json['passwordType']), deviceUuid: json['deviceUuid'], + authorizerEmail: json['authorizerEmail'], + authorizerDate: json['authorizerDate'], + deviceName: json['deviceName'], ); } @@ -44,10 +53,13 @@ class PasswordModel { 'effectiveTime': effectiveTime, 'passwordCreated': passwordCreated, 'createdTime': createdTime, - 'passwodName': passwordName, // New field + 'passwordName': passwordName, // New field 'passwordStatus': passwordStatus, 'passwordType': passwordType, 'deviceUuid': deviceUuid, + 'authorizerEmail': authorizerEmail, + 'authorizerDate': authorizerDate, + 'deviceName': deviceName, }; } } diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index a63b02a3..bed27eea 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -1,67 +1,47 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; import 'package:syncrow_web/pages/common/custom_table.dart'; import 'package:syncrow_web/pages/common/date_time_widget.dart'; +import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/app_enum.dart'; -import 'package:syncrow_web/utils/constants/routes_const.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; -class AccessManagementPage extends StatelessWidget { +class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout { const AccessManagementPage({super.key}); + @override Widget build(BuildContext context) { - Size size = MediaQuery.of(context).size; + final isLargeScreen = isLargeScreenSize(context); + final isSmallScreen = isSmallScreenSize(context); + final isHalfMediumScreen = isHafMediumScreenSize(context); + final padding = + isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15); + return WebScaffold( - enableMenuSideba: false, - appBarTitle: Row( - children: [ - Text( - 'Access Management', - style: Theme.of(context).textTheme.headlineLarge, - ) - ], - ), - appBarBody: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Physical Access', - style: Theme.of(context).textTheme.headlineMedium!.copyWith(color: Colors.white), - ), - Row( - children: [ - InkWell( - onTap: () { - context.go(RoutesConst.home); - }, - child: SvgPicture.asset( - height: 20, - width: 20, - Assets.grid, - ), - ), - const SizedBox( - width: 10, - ) - ], - ), - ], + enableMenuSidebar: false, + appBarTitle: FittedBox( + child: Text( + 'Access Management', + style: Theme.of(context).textTheme.headlineLarge, ), - ], + ), + rightBody: const NavigateHomeGridView(), scaffoldBody: BlocProvider( - create: (BuildContext context) => AccessBloc()..add(FetchTableData()), + create: (BuildContext context) => + AccessBloc()..add(FetchTableData()), child: BlocConsumer( listener: (context, state) {}, builder: (context, state) { @@ -70,196 +50,42 @@ class AccessManagementPage extends StatelessWidget { return state is AccessLoaded ? const Center(child: CircularProgressIndicator()) : Container( - padding: EdgeInsets.all(30), - height: size.height, - width: size.width, + padding: padding, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - decoration: containerDecoration, - height: size.height * 0.05, - child: Flexible( - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: BlocProvider.of(context).tabs.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final isSelected = index == - BlocProvider.of(context).selectedIndex; - return InkWell( - onTap: () { - BlocProvider.of(context) - .add(TabChangedEvent(index)); - }, - child: Container( - decoration: BoxDecoration( - color: ColorsManager.boxColor, - border: Border.all( - color: isSelected ? Colors.blue : Colors.transparent, - width: 2.0, - ), - borderRadius: index == 0 - ? const BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10)) - : index == 3 - ? const BorderRadius.only( - topRight: Radius.circular(10), - bottomRight: Radius.circular(10)) - : null, - ), - padding: const EdgeInsets.only(left: 10, right: 10), - child: Center( - child: Text( - BlocProvider.of(context).tabs[index], - style: TextStyle( - color: isSelected ? Colors.blue : Colors.black, - ), - ), - ), - ), - ); - }, - ), - ), - ), - const SizedBox( - height: 20, - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - textBaseline: TextBaseline.ideographic, - children: [ - Container( - width: size.width * 0.15, - child: CustomWebTextField( - controller: accessBloc.passwordName, - isRequired: true, - textFieldName: 'Name', - description: '', - ), - ), - const SizedBox( - width: 15, - ), - DateTimeWebWidget( - icon: Assets.calendarIcon, - isRequired: false, - title: 'Access Time', - size: size, - endTime: () { - accessBloc.add(SelectTime(context: context, isStart: false)); - }, - startTime: () { - accessBloc.add(SelectTime(context: context, isStart: true)); - }, - firstString: BlocProvider.of(context).startTime, - secondString: BlocProvider.of(context).endTime, - ), - const SizedBox( - width: 15, - ), - SizedBox( - width: size.width * 0.06, - child: Container( - decoration: containerDecoration, - child: DefaultButton( - onPressed: () { - accessBloc.add(FilterDataEvent( - selectedTabIndex: BlocProvider.of( - context) - .selectedIndex, // Pass the selected tab index - passwordName: - accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - borderRadius: 9, - child: const Text('Search'))), - ), - const SizedBox( - width: 10, - ), - SizedBox( - width: size.width * 0.06, - child: Container( - decoration: containerDecoration, - child: DefaultButton( - onPressed: () { - accessBloc.add(ResetSearch()); - }, - backgroundColor: ColorsManager.whiteColors, - borderRadius: 9, - child: Text( - 'Reset', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Colors.black), - ), - ), - ), - ), - ], - ), - const SizedBox( - height: 20, - ), - Wrap( - children: [ - Container( - width: size.width * 0.15, - decoration: containerDecoration, - child: DefaultButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return const VisitorPasswordDialog(); - }, - ).then((v) { - if (v != null) { - accessBloc.add(FetchTableData()); - } - }); - }, - borderRadius: 8, - child: const Text('+ Create Visitor Password ')), - ), - const SizedBox( - width: 10, - ), - Container( - width: size.width * 0.12, - decoration: containerDecoration, - child: DefaultButton( - borderRadius: 8, - backgroundColor: ColorsManager.whiteColors, - child: Text( - 'Admin Password', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Colors.black), - ))) - ], - ), - const SizedBox( - height: 20, + FilterWidget( + size: MediaQuery.of(context).size, + tabs: accessBloc.tabs, + selectedIndex: accessBloc.selectedIndex, + onTabChanged: (index) { + accessBloc.add(TabChangedEvent(index)); + }, ), + const SizedBox(height: 20), + if (isSmallScreen || isHalfMediumScreen) + _buildSmallSearchFilters(context, accessBloc) + else + _buildNormalSearchWidgets(context, accessBloc), + const SizedBox(height: 20), + _buildVisitorAdminPasswords(context, accessBloc), + const SizedBox(height: 20), Expanded( child: DynamicTable( + tableName: 'AccessManagement', + uuidIndex: 1, + withSelectAll: true, isEmpty: filteredData.isEmpty, withCheckBox: false, - size: size, + size: MediaQuery.of(context).size, cellDecoration: containerDecoration, headers: const [ 'Name', 'Access Type', - 'Access Period', + 'Access Start', + 'Access End', 'Accessible Device', 'Authorizer', 'Authorization Date & Time', @@ -267,21 +93,212 @@ class AccessManagementPage extends StatelessWidget { ], data: filteredData.map((item) { return [ - item.passwordName.toString(), + item.passwordName, item.passwordType.value, - ('${accessBloc.timestampToDate(item.effectiveTime)} - ${accessBloc.timestampToDate(item.invalidTime)}'), - item.deviceUuid.toString(), - '', - '', + accessBloc + .timestampToDate(item.effectiveTime), + accessBloc + .timestampToDate(item.invalidTime), + item.deviceName.toString(), + item.authorizerEmail.toString(), + accessBloc + .timestampToDate(item.invalidTime), item.passwordStatus.value, ]; }).toList(), - ) - // : const Center(child: CircularProgressIndicator()), - ) + )), ], ), ); }))); } + + Wrap _buildVisitorAdminPasswords( + BuildContext context, AccessBloc accessBloc) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: [ + Container( + width: 205, + height: 42, + decoration: containerDecoration, + child: DefaultButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const VisitorPasswordDialog(); + }, + ).then((v) { + if (v != null) { + accessBloc.add(FetchTableData()); + } + }); + }, + borderRadius: 8, + child: Text( + 'Create Visitor Password ', + style: context.textTheme.titleSmall! + .copyWith(color: Colors.white, fontSize: 12), + )), + ), + Container( + width: 133, + height: 42, + decoration: containerDecoration, + child: DefaultButton( + borderRadius: 8, + backgroundColor: ColorsManager.whiteColors, + child: Text( + 'Admin Password', + style: context.textTheme.titleSmall! + .copyWith(color: Colors.black, fontSize: 12), + )), + ), + ], + ); + } + + Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) { + // TimeOfDay _selectedTime = TimeOfDay.now(); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.ideographic, + children: [ + SizedBox( + width: 250, + child: CustomWebTextField( + controller: accessBloc.passwordName, + height: 43, + isRequired: false, + textFieldName: 'Name', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + ), + ), + const SizedBox(width: 15), + SizedBox( + width: 250, + child: CustomWebTextField( + controller: accessBloc.emailAuthorizer, + height: 43, + isRequired: false, + textFieldName: 'Authorizer', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + ), + ), + const SizedBox(width: 15), + SizedBox( + child: DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: MediaQuery.of(context).size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString: BlocProvider.of(context).startTime, + secondString: BlocProvider.of(context).endTime, + ), + ), + const SizedBox(width: 15), + SearchResetButtons( + onSearch: () { + accessBloc.add(FilterDataEvent( + emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + onReset: () { + accessBloc.add(ResetSearch()); + }, + ), + ], + ); + } + + Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) { + return Wrap( + spacing: 20, + runSpacing: 10, + children: [ + SizedBox( + width: 300, + child: CustomWebTextField( + controller: accessBloc.passwordName, + isRequired: true, + height: 40, + textFieldName: 'Name', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }), + ), + DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: MediaQuery.of(context).size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString: BlocProvider.of(context).startTime, + secondString: BlocProvider.of(context).endTime, + ), + SearchResetButtons( + onSearch: () { + accessBloc.add(FilterDataEvent( + emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + onReset: () { + accessBloc.add(ResetSearch()); + }, + ), + ], + ); + } } diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index ea53fdd3..60cc2f86 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -34,14 +35,15 @@ class AuthBloc extends Bloc { TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); + final forgetEmailKey = GlobalKey(); + final forgetRegionKey = GlobalKey(); late bool checkValidate = false; Timer? _timer; int _remainingTime = 0; List? regionList = [RegionModel(name: 'name', id: 'id')]; - Future _onStartTimer( - StartTimerEvent event, Emitter emit) async { + Future _onStartTimer(StartTimerEvent event, Emitter emit) async { if (_validateInputs(emit)) return; if (_timer != null && _timer!.isActive) { return; @@ -49,8 +51,35 @@ class AuthBloc extends Bloc { _remainingTime = 1; add(UpdateTimerEvent( remainingTime: _remainingTime, isButtonEnabled: false)); - _remainingTime = (await AuthenticationAPI.sendOtp( - email: forgetEmailController.text, regionUuid: regionUuid))!; + try { + forgetEmailValidate = ''; + _remainingTime = (await AuthenticationAPI.sendOtp( + email: forgetEmailController.text, regionUuid: regionUuid))!; + } on DioException catch (e) { + if (e.response!.statusCode == 400) { + final errorData = e.response!.data; + String errorMessage = errorData['message']; + if (errorMessage == 'User not found') { + validate = 'Invalid Credential'; + emit(AuthInitialState()); + return 1; + } else { + validate = ''; + _remainingTime = errorData['data']['cooldown'] ?? 1; + emit(AuthInitialState()); + } + } else { + emit(AuthInitialState()); + + return 1; + } + emit(AuthInitialState()); + } catch (e) { + emit(AuthInitialState()); + + return 1; + } + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { _remainingTime--; if (_remainingTime <= 0) { @@ -70,32 +99,28 @@ class AuthBloc extends Bloc { Future changePassword( ChangePasswordEvent event, Emitter emit) async { + emit(LoadingForgetState()); try { - emit(LoadingForgetState()); var response = await AuthenticationAPI.verifyOtp( email: forgetEmailController.text, otpCode: forgetOtp.text); if (response == true) { await AuthenticationAPI.forgetPassword( + otpCode: forgetOtp.text, password: forgetPasswordController.text, email: forgetEmailController.text); _timer?.cancel(); emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); emit(SuccessForgetState()); - } else if (response == "You entered wrong otp") { - forgetValidate = 'Wrong one time password.'; - emit(AuthInitialState()); - } else if (response == "OTP expired") { - forgetValidate = 'One time password has been expired.'; - emit(AuthInitialState()); } - } catch (failure) { - // forgetValidate='Invalid Credentials!'; + } on DioException catch (e) { + final errorData = e.response!.data; + String errorMessage = + errorData['error']['message'] ?? 'something went wrong'; + validate = errorMessage; emit(AuthInitialState()); - // emit(FailureForgetState(error: failure.toString())); } } -//925207 String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -120,6 +145,7 @@ class AuthBloc extends Bloc { String otpCode = ''; String validate = ''; String forgetValidate = ''; + String forgetEmailValidate = ''; String regionUuid = ''; static Token token = Token.emptyConstructor(); static UserModel? user; @@ -127,7 +153,6 @@ class AuthBloc extends Bloc { void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); - if (isChecked) { try { if (event.username.isEmpty || event.password.isEmpty) { @@ -135,6 +160,7 @@ class AuthBloc extends Bloc { emit(const LoginFailure(error: 'Something went wrong')); return; } + token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( email: event.username, @@ -143,10 +169,10 @@ class AuthBloc extends Bloc { ); } catch (failure) { validate = 'Invalid Credentials!'; - emit(AuthInitialState()); - // emit(const LoginFailure(error: 'Something went wrong')); + emit(LoginInitial()); return; } + if (token.accessTokenIsNotEmpty) { FlutterSecureStorage storage = const FlutterSecureStorage(); await storage.write( @@ -155,9 +181,9 @@ class AuthBloc extends Bloc { key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString()); user = UserModel.fromToken(token); - debugPrint(token.accessToken); loginEmailController.clear(); loginPasswordController.clear(); + debugPrint("token " + token.accessToken); emit(LoginSuccess()); } else { emit(const LoginFailure(error: 'Something went wrong')); @@ -177,15 +203,15 @@ class AuthBloc extends Bloc { emit(LoginInitial()); } - checkOtpCode( - ChangePasswordEvent event, - Emitter emit, - ) async { - emit(LoadingForgetState()); - await AuthenticationAPI.verifyOtp( - email: forgetEmailController.text, otpCode: forgetOtp.text); - emit(SuccessForgetState()); - } + // checkOtpCode( + // ChangePasswordEvent event, + // Emitter emit, + // ) async { + // emit(LoadingForgetState()); + // await AuthenticationAPI.verifyOtp( + // email: forgetEmailController.text, otpCode: forgetOtp.text); + // emit(SuccessForgetState()); + // } void _passwordVisible(PasswordVisibleEvent event, Emitter emit) { emit(AuthLoading()); @@ -208,9 +234,10 @@ class AuthBloc extends Bloc { return 'Email is required'; } else if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { return 'Enter a valid email address'; - } else if (regionUuid == '') { - return 'Please select your region'; } + // else if (regionUuid == '') { + // return 'Please select your region'; + // } validate = ''; return null; } @@ -226,7 +253,9 @@ class AuthBloc extends Bloc { emit(LoadingForgetState()); final nameError = validateEmail(forgetEmailController.text); if (nameError != null) { - emit(FailureForgetState(error: nameError)); + forgetEmailValidate = nameError; + emit(AuthInitialState()); + // emit(FailureForgetState(error: nameError)); return true; } return false; @@ -348,6 +377,7 @@ class AuthBloc extends Bloc { try { emit(AuthLoading()); regionUuid = event.val; + add(CheckEnableEvent()); emit(AuthInitialState()); } catch (e) { emit(FailureForgetState(error: e.toString())); @@ -404,4 +434,9 @@ class AuthBloc extends Bloc { forgetValidate = ''; emit(LoginInitial()); } + + static logout() { + const storage = FlutterSecureStorage(); + storage.deleteAll(); + } } diff --git a/lib/pages/auth/bloc/auth_event.dart b/lib/pages/auth/bloc/auth_event.dart index a8786cbc..fa7e86b3 100644 --- a/lib/pages/auth/bloc/auth_event.dart +++ b/lib/pages/auth/bloc/auth_event.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; abstract class AuthEvent extends Equatable { const AuthEvent(); @@ -47,13 +46,16 @@ class StopTimerEvent extends AuthEvent {} class UpdateTimerEvent extends AuthEvent { final int remainingTime; final bool isButtonEnabled; - const UpdateTimerEvent( - {required this.remainingTime, required this.isButtonEnabled}); + const UpdateTimerEvent({required this.remainingTime, required this.isButtonEnabled}); } -class ChangePasswordEvent extends AuthEvent {} +class ChangePasswordEvent extends AuthEvent { -class SendOtpEvent extends AuthEvent {} +} + +class SendOtpEvent extends AuthEvent { + +} class PasswordVisibleEvent extends AuthEvent { final bool? newValue; diff --git a/lib/pages/auth/bloc/auth_state.dart b/lib/pages/auth/bloc/auth_state.dart index 8f2f6bd5..f3a95157 100644 --- a/lib/pages/auth/bloc/auth_state.dart +++ b/lib/pages/auth/bloc/auth_state.dart @@ -56,8 +56,7 @@ class TimerState extends AuthState { final bool isButtonEnabled; final int remainingTime; - const TimerState( - {required this.isButtonEnabled, required this.remainingTime}); + const TimerState({required this.isButtonEnabled, required this.remainingTime}); @override List get props => [isButtonEnabled, remainingTime]; @@ -65,12 +64,12 @@ class TimerState extends AuthState { class AuthError extends AuthState { final String message; - String? code; - AuthError({required this.message, this.code}); + final String? code; + const AuthError({required this.message, this.code}); } class AuthTokenError extends AuthError { - AuthTokenError({required super.message, super.code}); + const AuthTokenError({required super.message, super.code}); } class AuthSuccess extends AuthState {} diff --git a/lib/pages/auth/view/forget_password_page.dart b/lib/pages/auth/view/forget_password_page.dart index 63b4d0f9..0ab2c2df 100644 --- a/lib/pages/auth/view/forget_password_page.dart +++ b/lib/pages/auth/view/forget_password_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/auth/view/forget_password_mobile_page.dart'; import 'package:syncrow_web/pages/auth/view/forget_password_web_page.dart'; import 'package:syncrow_web/utils/responsive_layout.dart'; @@ -9,7 +8,6 @@ class ForgetPasswordPage extends StatelessWidget { @override Widget build(BuildContext context) { return const ResponsiveLayout( - desktopBody: ForgetPasswordWebPage(), - mobileBody: ForgetPasswordWebPage()); + desktopBody: ForgetPasswordWebPage(), mobileBody: ForgetPasswordWebPage()); } } diff --git a/lib/pages/auth/view/forget_password_web_page.dart b/lib/pages/auth/view/forget_password_web_page.dart index 907569ee..c04e7ee0 100644 --- a/lib/pages/auth/view/forget_password_web_page.dart +++ b/lib/pages/auth/view/forget_password_web_page.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -47,8 +48,7 @@ class ForgetPasswordWebPage extends StatelessWidget { late ScrollController _scrollController; _scrollController = ScrollController(); void _scrollToCenter() { - final double middlePosition = - _scrollController.position.maxScrollExtent / 2; + final double middlePosition = _scrollController.position.maxScrollExtent / 2; _scrollController.animateTo( middlePosition, duration: const Duration(seconds: 1), @@ -65,8 +65,7 @@ class ForgetPasswordWebPage extends StatelessWidget { second: Center( child: Stack( children: [ - if (state is AuthLoading) - const Center(child: CircularProgressIndicator()), + if (state is AuthLoading) const Center(child: CircularProgressIndicator()), ListView( shrinkWrap: true, controller: _scrollController, @@ -96,21 +95,16 @@ class ForgetPasswordWebPage extends StatelessWidget { child: Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), - borderRadius: - const BorderRadius.all(Radius.circular(30)), - border: Border.all( - color: - ColorsManager.graysColor.withOpacity(0.2)), + borderRadius: const BorderRadius.all(Radius.circular(30)), + border: Border.all(color: ColorsManager.graysColor.withOpacity(0.2)), ), child: Form( key: forgetBloc.forgetFormKey, child: Padding( padding: EdgeInsets.symmetric( - horizontal: size.width * 0.02, - vertical: size.width * 0.003), + horizontal: size.width * 0.02, vertical: size.width * 0.003), child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), @@ -127,102 +121,55 @@ class ForgetPasswordWebPage extends StatelessWidget { style: Theme.of(context) .textTheme .bodySmall! - .copyWith( - fontSize: 14, - fontWeight: FontWeight.w400), + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), const SizedBox(height: 10), Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - Text( - "Country/Region", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - fontSize: 14, - fontWeight: FontWeight.w400), - ), const SizedBox(height: 10), - SizedBox( - child: DropdownButtonFormField( - validator: forgetBloc.validateRegion, - icon: const Icon( - Icons.keyboard_arrow_down_outlined, - ), - decoration: - textBoxDecoration()!.copyWith( - hintText: null, - ), - hint: SizedBox( - width: size.width * 0.12, - child: const Align( - alignment: Alignment.centerLeft, - child: Text( - 'Select your region/country', - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ), - isDense: true, - style: const TextStyle( - color: Colors.black), - items: forgetBloc.regionList! - .map((RegionModel region) { - return DropdownMenuItem( - value: region.id, - child: SizedBox( - width: size.width * 0.06, - child: Text(region.name)), - ); - }).toList(), - onChanged: (String? value) { - forgetBloc.add(SelectRegionEvent( - val: value!, - )); - }, - ), - ) + Form( + key: forgetBloc.forgetRegionKey, + child: SizedBox( + child: + _buildDropdownField(context, forgetBloc, size))) ], ), const SizedBox(height: 20), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Account", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - fontSize: 14, - fontWeight: FontWeight.w400), - ), - const SizedBox(height: 10), - SizedBox( - child: TextFormField( - validator: forgetBloc.validateEmail, - controller: - forgetBloc.forgetEmailController, - decoration: textBoxDecoration()! - .copyWith( - hintText: 'Enter your email'), - style: const TextStyle( - color: Colors.black), - ), - ), - ], - ), + Form( + key: forgetBloc.forgetEmailKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Account", + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 14, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + SizedBox( + child: TextFormField( + controller: forgetBloc.forgetEmailController, + validator: forgetBloc.validateEmail, + decoration: textBoxDecoration()!.copyWith( + hintText: 'Enter your email', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400), + ), + style: const TextStyle(color: Colors.black), + ), + ), + ], + )), const SizedBox(height: 20.0), Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Text( @@ -230,59 +177,64 @@ class ForgetPasswordWebPage extends StatelessWidget { style: Theme.of(context) .textTheme .bodySmall! - .copyWith( - fontSize: 14, - fontWeight: FontWeight.w400), + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), const SizedBox(height: 10), SizedBox( child: TextFormField( validator: forgetBloc.validateCode, - keyboardType: - TextInputType.visiblePassword, + keyboardType: TextInputType.visiblePassword, controller: forgetBloc.forgetOtp, - decoration: - textBoxDecoration()!.copyWith( + decoration: textBoxDecoration()!.copyWith( hintText: 'Enter Code', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400), suffixIcon: SizedBox( width: 100, child: Center( child: InkWell( onTap: state is TimerState && - !state - .isButtonEnabled && - state.remainingTime != - 1 + !state.isButtonEnabled && + state.remainingTime != 1 ? null : () { - forgetBloc.add( - StartTimerEvent()); + if (forgetBloc + .forgetEmailKey.currentState! + .validate() || + forgetBloc + .forgetRegionKey.currentState! + .validate()) { + if (forgetBloc + .forgetRegionKey.currentState! + .validate()) { + forgetBloc.add(StartTimerEvent()); + } + } }, child: Text( 'Get Code ${state is TimerState && !state.isButtonEnabled && state.remainingTime != 1 ? "(${forgetBloc.formattedTime(state.remainingTime)}) " : ""}', style: TextStyle( - color: state - is TimerState && - !state - .isButtonEnabled + color: state is TimerState && + !state.isButtonEnabled ? Colors.grey - : ColorsManager - .btnColor, + : ColorsManager.btnColor, ), ), ), ), ), ), - style: const TextStyle( - color: Colors.black), + style: const TextStyle(color: Colors.black), ), ), if (forgetBloc.forgetValidate != '') // Check if there is a validation message Padding( - padding: - const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 8.0), child: Text( forgetBloc.forgetValidate, style: const TextStyle( @@ -295,8 +247,7 @@ class ForgetPasswordWebPage extends StatelessWidget { ), const SizedBox(height: 20.0), Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Text( @@ -304,25 +255,40 @@ class ForgetPasswordWebPage extends StatelessWidget { style: Theme.of(context) .textTheme .bodySmall! - .copyWith( - fontSize: 14, - fontWeight: FontWeight.w400), + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), const SizedBox(height: 10), SizedBox( child: TextFormField( - validator: - forgetBloc.passwordValidator, - keyboardType: - TextInputType.visiblePassword, - controller: forgetBloc - .forgetPasswordController, - decoration: - textBoxDecoration()!.copyWith( + obscureText: forgetBloc.obscureText, + keyboardType: TextInputType.visiblePassword, + validator: forgetBloc.passwordValidator, + controller: forgetBloc.forgetPasswordController, + decoration: textBoxDecoration()!.copyWith( + suffixIcon: IconButton( + onPressed: () { + forgetBloc.add(PasswordVisibleEvent( + newValue: forgetBloc.obscureText)); + }, + icon: SizedBox( + child: SvgPicture.asset( + forgetBloc.obscureText + ? Assets.visiblePassword + : Assets.invisiblePassword, + height: 15, + width: 15, + ), + ), + ), hintText: 'At least 8 characters', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400), ), - style: const TextStyle( - color: Colors.black), + style: const TextStyle(color: Colors.black), ), ), ], @@ -332,22 +298,24 @@ class ForgetPasswordWebPage extends StatelessWidget { ), const SizedBox(height: 20.0), Row( - crossAxisAlignment: - CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: size.width * 0.2, child: DefaultButton( - backgroundColor: - ColorsManager.btnColor, + backgroundColor: ColorsManager.btnColor, child: const Text('Submit'), onPressed: () { - if (forgetBloc - .forgetFormKey.currentState! - .validate()) { - forgetBloc - .add(ChangePasswordEvent()); + if (forgetBloc.forgetFormKey.currentState!.validate() || + forgetBloc.forgetEmailKey.currentState! + .validate()) { + if (forgetBloc.forgetEmailKey.currentState! + .validate() && + forgetBloc.forgetFormKey.currentState! + .validate()) { + forgetBloc.add(ChangePasswordEvent()); + } } }, ), @@ -355,41 +323,37 @@ class ForgetPasswordWebPage extends StatelessWidget { ], ), const SizedBox(height: 10.0), - SizedBox( - child: Text( - forgetBloc.validate, - style: const TextStyle( - fontWeight: FontWeight.w700, - color: ColorsManager.red), + Center( + child: SizedBox( + child: Text( + forgetBloc.validate, + style: const TextStyle( + fontWeight: FontWeight.w700, color: ColorsManager.red), + ), ), ), - SizedBox( + const SizedBox( height: 10, ), SizedBox( - width: size.width * 0.2, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - const Flexible( - child: Text( - "Do you have an account? ", - style: TextStyle(color: Colors.white), - )), - InkWell( - onTap: () { - forgetBloc.add(StopTimerEvent()); - Navigator.pop(context); - }, - child: const Flexible( - child: Text( - "Sign in", - )), - ), - ], + child: Center( + child: Wrap( + children: [ + const Text( + "Do you have an account? ", + style: TextStyle(color: Colors.white), + ), + InkWell( + onTap: () { + forgetBloc.add(StopTimerEvent()); + Navigator.pop(context); + }, + child: const Text( + "Sign in", + ), + ), + ], + ), ), ), const SizedBox(height: 15.0), @@ -410,4 +374,152 @@ class ForgetPasswordWebPage extends StatelessWidget { ), )); } + + Widget _buildDropdownField(BuildContext context, AuthBloc loginBloc, Size size) { + final TextEditingController textEditingController = TextEditingController(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Country/Region", + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 10), + SizedBox( + width: size.width * 0.9, + child: FormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select a country/region'; + } + return null; + }, + builder: (FormFieldState field) { + return InputDecorator( + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 2, vertical: 10), + errorText: field.errorText, + filled: true, // Ensure the dropdown is filled with the background color + fillColor: ColorsManager.boxColor, // Match the dropdown container color + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: field.hasError ? Colors.red : Colors.transparent, + width: 1.5, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: field.hasError ? Colors.red : ColorsManager.grayColor, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: field.hasError ? Colors.red : ColorsManager.grayColor, + width: 1.5, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide( + color: Colors.red, + width: 1.5, + ), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + hint: Text( + 'Select your region/country', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + ), + overflow: TextOverflow.ellipsis, + ), + items: loginBloc.regionList!.map((RegionModel region) { + return DropdownMenuItem( + value: region.id, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + region.name, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ); + }).toList(), + value: loginBloc.regionList!.any((region) => region.id == loginBloc.regionUuid) + ? loginBloc.regionUuid + : null, + onChanged: (String? value) { + if (value != null) { + loginBloc.add(SelectRegionEvent(val: value)); + field.didChange(value); // Notify the form field of the change + } + }, + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.symmetric(horizontal: 16), + height: 40, + width: double.infinity, + ), + dropdownStyleData: DropdownStyleData( + maxHeight: size.height * 0.70, + decoration: BoxDecoration( + color: ColorsManager + .boxColor, // Match dropdown background color to the container + borderRadius: BorderRadius.circular(8.0), + ), + ), + menuItemStyleData: const MenuItemStyleData( + height: 40, + ), + dropdownSearchData: DropdownSearchData( + searchController: textEditingController, + searchInnerWidgetHeight: 50, + searchInnerWidget: Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: TextFormField( + style: const TextStyle(color: Colors.black), + controller: textEditingController, + decoration: textBoxDecoration()!.copyWith( + errorStyle: const TextStyle(height: 0), + contentPadding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 10, + ), + ), + ), + ), + searchMatchFn: (item, searchValue) { + final regionName = (item.child as Text).data?.toLowerCase() ?? ''; + final search = searchValue.toLowerCase().trim(); + return regionName.contains(search); + }, + ), + onMenuStateChange: (isOpen) { + if (!isOpen) { + textEditingController.clear(); + } + }, + ), + ), + ); + }, + ), + ), + ], + ); + } } diff --git a/lib/pages/auth/view/login_mobile_page.dart b/lib/pages/auth/view/login_mobile_page.dart index 1413db5d..1a5c8358 100644 --- a/lib/pages/auth/view/login_mobile_page.dart +++ b/lib/pages/auth/view/login_mobile_page.dart @@ -1,8 +1,8 @@ -import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_event.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_state.dart'; @@ -11,7 +11,7 @@ import 'package:syncrow_web/pages/auth/view/forget_password_page.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/pages/home/view/home_page.dart'; +import 'package:syncrow_web/utils/constants/routes_const.dart'; import 'package:syncrow_web/utils/style.dart'; class LoginMobilePage extends StatelessWidget { @@ -25,10 +25,7 @@ class LoginMobilePage extends StatelessWidget { listener: (context, state) { if (state is LoginSuccess) { // Navigate to home screen after successful login - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => HomePage()), - ); + context.go(RoutesConst.home, extra: {'clearHistory': true}); } else if (state is LoginFailure) { // Show error message ScaffoldMessenger.of(context).showSnackBar( @@ -52,8 +49,6 @@ class LoginMobilePage extends StatelessWidget { Widget _buildLoginForm(BuildContext context) { final loginBloc = BlocProvider.of(context); - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); return Center( child: Stack( children: [ @@ -102,11 +97,8 @@ class LoginMobilePage extends StatelessWidget { padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), - borderRadius: - const BorderRadius.all(Radius.circular(30)), - border: Border.all( - color: - ColorsManager.graysColor.withOpacity(0.2))), + borderRadius: const BorderRadius.all(Radius.circular(30)), + border: Border.all(color: ColorsManager.graysColor.withOpacity(0.2))), child: Form( key: loginBloc.loginFormKey, child: Column( @@ -117,9 +109,7 @@ class LoginMobilePage extends StatelessWidget { const Text( 'Login', style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold), + color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 30), Column( @@ -148,8 +138,7 @@ class LoginMobilePage extends StatelessWidget { ), isDense: true, style: const TextStyle(color: Colors.black), - items: loginBloc.regionList! - .map((RegionModel region) { + items: loginBloc.regionList!.map((RegionModel region) { return DropdownMenuItem( value: region.name, child: Text(region.name), @@ -173,8 +162,8 @@ class LoginMobilePage extends StatelessWidget { child: TextFormField( validator: loginBloc.validateEmail, controller: loginBloc.loginEmailController, - decoration: textBoxDecoration()! - .copyWith(hintText: 'Enter your email'), + decoration: + textBoxDecoration()!.copyWith(hintText: 'Enter your email'), style: const TextStyle(color: Colors.black), ), ), @@ -194,8 +183,7 @@ class LoginMobilePage extends StatelessWidget { validator: loginBloc.validatePassword, obscureText: loginBloc.obscureText, keyboardType: TextInputType.visiblePassword, - controller: - loginBloc.loginPasswordController, + controller: loginBloc.loginPasswordController, decoration: textBoxDecoration()!.copyWith( hintText: 'At least 8 characters', ), @@ -213,16 +201,13 @@ class LoginMobilePage extends StatelessWidget { children: [ InkWell( onTap: () { - Navigator.of(context) - .push(MaterialPageRoute( - builder: (context) => - const ForgetPasswordPage(), + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const ForgetPasswordPage(), )); }, child: Text( "Forgot Password?", - style: - Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.bodySmall, ), ), ], @@ -233,15 +218,13 @@ class LoginMobilePage extends StatelessWidget { Transform.scale( scale: 1.2, // Adjust the scale as needed child: Checkbox( - fillColor: MaterialStateProperty.all( - Colors.white), + fillColor: MaterialStateProperty.all(Colors.white), activeColor: Colors.white, value: loginBloc.isChecked, checkColor: Colors.black, shape: const CircleBorder(), onChanged: (bool? newValue) { - loginBloc.add( - CheckBoxEvent(newValue: newValue)); + loginBloc.add(CheckBoxEvent(newValue: newValue)); }, ), ), @@ -250,37 +233,30 @@ class LoginMobilePage extends StatelessWidget { child: RichText( text: TextSpan( text: 'Agree to ', - style: - const TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), children: [ TextSpan( text: '(Terms of Service)', - style: const TextStyle( - color: Colors.black), + style: const TextStyle(color: Colors.black), recognizer: TapGestureRecognizer() ..onTap = () { - loginBloc.launchURL( - 'https://example.com/terms'); + loginBloc.launchURL('https://example.com/terms'); }, ), TextSpan( text: ' (Legal Statement)', - style: const TextStyle( - color: Colors.black), + style: const TextStyle(color: Colors.black), recognizer: TapGestureRecognizer() ..onTap = () { - loginBloc.launchURL( - 'https://example.com/legal'); + loginBloc.launchURL('https://example.com/legal'); }, ), TextSpan( text: ' (Privacy Statement)', - style: const TextStyle( - color: Colors.black), + style: const TextStyle(color: Colors.black), recognizer: TapGestureRecognizer() ..onTap = () { - loginBloc.launchURL( - 'https://example.com/privacy'); + loginBloc.launchURL('https://example.com/privacy'); }, ), ], @@ -297,15 +273,12 @@ class LoginMobilePage extends StatelessWidget { : ColorsManager.grayColor, child: const Text('Sign in'), onPressed: () { - if (loginBloc.loginFormKey.currentState! - .validate()) { + if (loginBloc.loginFormKey.currentState!.validate()) { loginBloc.add( LoginButtonPressed( regionUuid: '', - username: - loginBloc.loginEmailController.text, - password: loginBloc - .loginPasswordController.text, + username: loginBloc.loginEmailController.text, + password: loginBloc.loginPasswordController.text, ), ); } @@ -320,8 +293,7 @@ class LoginMobilePage extends StatelessWidget { Flexible( child: Text( "Don't you have an account? ", - style: TextStyle( - color: Colors.white, fontSize: 13), + style: TextStyle(color: Colors.white, fontSize: 13), )), Text( "Sign up", diff --git a/lib/pages/auth/view/login_web_page.dart b/lib/pages/auth/view/login_web_page.dart index 7e9123ee..555f8f4a 100644 --- a/lib/pages/auth/view/login_web_page.dart +++ b/lib/pages/auth/view/login_web_page.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,6 +14,7 @@ import 'package:syncrow_web/pages/common/first_layer.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/style.dart'; class LoginWebPage extends StatefulWidget { @@ -22,7 +24,7 @@ class LoginWebPage extends StatefulWidget { State createState() => _LoginWebPageState(); } -class _LoginWebPageState extends State { +class _LoginWebPageState extends State with HelperResponsiveLayout { @override Widget build(BuildContext context) { return Scaffold( @@ -31,7 +33,7 @@ class _LoginWebPageState extends State { child: BlocConsumer( listener: (context, state) { if (state is LoginSuccess) { - context.go(RoutesConst.home); + GoRouter.of(context).go(RoutesConst.home); } else if (state is LoginFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -50,9 +52,12 @@ class _LoginWebPageState extends State { Widget _buildLoginForm(BuildContext context, AuthState state) { final loginBloc = BlocProvider.of(context); + final isSmallScreen = isSmallScreenSize(context); + final isMediumScreen = isMediumScreenSize(context); Size size = MediaQuery.of(context).size; late ScrollController _scrollController; _scrollController = ScrollController(); + void _scrollToCenter() { final double middlePosition = _scrollController.position.maxScrollExtent / 2; _scrollController.animateTo( @@ -65,6 +70,7 @@ class _LoginWebPageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToCenter(); }); + return Stack( children: [ FirstLayer( @@ -74,356 +80,52 @@ class _LoginWebPageState extends State { shrinkWrap: true, children: [ Container( + width: 400, padding: EdgeInsets.all(size.width * 0.02), - margin: EdgeInsets.all(size.width * 0.09), + margin: EdgeInsets.all(size.width * 0.05), decoration: BoxDecoration( color: Colors.black.withOpacity(0.3), borderRadius: const BorderRadius.all(Radius.circular(20)), ), child: Center( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - Expanded( - flex: 3, - child: SvgPicture.asset( - Assets.loginLogo, - ), - ), - const Spacer(), - Expanded( - flex: 3, - child: Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(30)), - border: Border.all( - color: ColorsManager.graysColor.withOpacity(0.2))), - child: Form( - key: loginBloc.loginFormKey, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: size.width * 0.02, - vertical: size.width * 0.003), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 40), - Text('Login', - style: Theme.of(context).textTheme.headlineLarge), - SizedBox(height: size.height * 0.03), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Country/Region", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - fontSize: 14, fontWeight: FontWeight.w400), - ), - const SizedBox( - height: 10, - ), - SizedBox( - child: DropdownButtonFormField( - padding: EdgeInsets.zero, - value: loginBloc.regionList!.any((region) => - region.id == loginBloc.regionUuid) - ? loginBloc.regionUuid - : null, - validator: loginBloc.validateRegion, - icon: const Icon( - Icons.keyboard_arrow_down_outlined, - ), - decoration: textBoxDecoration()!.copyWith( - errorStyle: const TextStyle(height: 0), - hintText: null, - ), - hint: SizedBox( - width: size.width * 0.12, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - 'Select your region/country', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: ColorsManager.grayColor, - fontWeight: FontWeight.w400), - overflow: TextOverflow.ellipsis, - ), - ), - ), - isDense: true, - style: const TextStyle(color: Colors.black), - items: - loginBloc.regionList!.map((RegionModel region) { - return DropdownMenuItem( - value: region.id, - child: SizedBox( - width: size.width * 0.08, - child: Text(region.name)), - ); - }).toList(), - onChanged: (String? value) { - loginBloc.add(CheckEnableEvent()); - loginBloc.add(SelectRegionEvent(val: value!)); - }, - ), - ) - ], - ), - const SizedBox(height: 20.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Email", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - fontSize: 14, fontWeight: FontWeight.w400), - ), - const SizedBox( - height: 10, - ), - SizedBox( - child: TextFormField( - onChanged: (value) { - loginBloc.add(CheckEnableEvent()); - // print(loginBloc.checkEnable()); - }, - validator: loginBloc.loginValidateEmail, - controller: loginBloc.loginEmailController, - decoration: textBoxDecoration()!.copyWith( - errorStyle: const TextStyle( - height: 0), // Hide the error text space - hintText: 'Enter your email address', - hintStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: ColorsManager.grayColor, - fontWeight: FontWeight.w400)), - style: const TextStyle(color: Colors.black), - ), - ), - ], - ), - const SizedBox(height: 20.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Password", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - fontSize: 14, fontWeight: FontWeight.w400), - ), - const SizedBox( - height: 10, - ), - SizedBox( - child: TextFormField( - onChanged: (value) { - loginBloc.add(CheckEnableEvent()); - }, - validator: loginBloc.validatePassword, - obscureText: loginBloc.obscureText, - keyboardType: TextInputType.visiblePassword, - controller: loginBloc.loginPasswordController, - decoration: textBoxDecoration()!.copyWith( - hintText: 'At least 8 characters', - hintStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: ColorsManager.grayColor, - fontWeight: FontWeight.w400), - suffixIcon: IconButton( - onPressed: () { - loginBloc.add(PasswordVisibleEvent( - newValue: loginBloc.obscureText)); - }, - icon: SizedBox( - child: SvgPicture.asset( - loginBloc.obscureText - ? Assets.visiblePassword - : Assets.invisiblePassword, - height: 15, - width: 15, - ), - ), - ), - errorStyle: const TextStyle( - height: 0), // Hide the error text space - ), - style: const TextStyle(color: Colors.black), - ), - ), - ], - ), - const SizedBox( - height: 20, - ), - SizedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - InkWell( - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => - const ForgetPasswordPage(), - )); - }, - child: Text( - "Forgot Password?", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.w400), - ), - ), - ], - ), - ), - const SizedBox( - height: 20, - ), - Row( - children: [ - Transform.scale( - scale: 1.2, // Adjust the scale as needed - child: Checkbox( - fillColor: - MaterialStateProperty.all(Colors.white), - activeColor: Colors.white, - value: loginBloc.isChecked, - checkColor: Colors.black, - shape: const CircleBorder(), - onChanged: (bool? newValue) { - loginBloc.add(CheckBoxEvent(newValue: newValue)); - }, - ), - ), - SizedBox( - width: size.width * 0.14, - child: RichText( - text: TextSpan( - text: 'Agree to ', - style: const TextStyle(color: Colors.white), - children: [ - TextSpan( - text: '(Terms of Service)', - style: const TextStyle( - color: Colors.black, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - loginBloc.launchURL( - 'https://example.com/terms'); - }, - ), - TextSpan( - text: ' (Legal Statement)', - style: const TextStyle(color: Colors.black), - recognizer: TapGestureRecognizer() - ..onTap = () { - loginBloc.launchURL( - 'https://example.com/legal'); - }, - ), - TextSpan( - text: ' (Privacy Statement)', - style: const TextStyle(color: Colors.black), - recognizer: TapGestureRecognizer() - ..onTap = () { - loginBloc.launchURL( - 'https://example.com/privacy'); - }, - ), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 20.0), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: size.width * 0.2, - child: DefaultButton( - enabled: loginBloc.checkValidate, - child: Text('Sign in', - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith( - fontSize: 14, - color: loginBloc.checkValidate - ? ColorsManager.whiteColors - : ColorsManager.whiteColors - .withOpacity(0.2), - )), - onPressed: () { - if (loginBloc.loginFormKey.currentState! - .validate()) { - loginBloc.add(LoginButtonPressed( - regionUuid: loginBloc.regionUuid, - username: loginBloc.loginEmailController.text, - password: - loginBloc.loginPasswordController.text, - )); - } else { - loginBloc.add(ChangeValidateEvent()); - } - }, - ), - ), - ], - ), - const SizedBox(height: 15.0), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - child: Text( - loginBloc.validate, - style: const TextStyle( - fontWeight: FontWeight.w700, - color: ColorsManager.red), - ), - ) - ], - ) - ], - ), + child: isSmallScreen || isMediumScreen + ? SizedBox( + width: 400, + child: Column( + // For small screens + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 300, + child: SvgPicture.asset( + Assets.loginLogo, ), - ))), - const Spacer(), - ], - ), + ), + const SizedBox(height: 20), + _buildLoginFormFields(context, loginBloc, size), + ], + ), + ) + : Row( + // For larger screens + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + Expanded( + flex: 2, + child: SvgPicture.asset( + Assets.loginLogo, + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: _buildLoginFormFields(context, loginBloc, size), + ), + const Spacer(), + ], + ), ), ), ], @@ -434,4 +136,358 @@ class _LoginWebPageState extends State { ], ); } + + Widget _buildLoginFormFields(BuildContext context, AuthBloc loginBloc, Size size) { + return Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(30)), + border: Border.all(color: ColorsManager.graysColor.withOpacity(0.2)), + ), + child: Form( + key: loginBloc.loginFormKey, + child: Padding( + padding: + EdgeInsets.symmetric(horizontal: size.width * 0.02, vertical: size.width * 0.003), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Text('Login', style: Theme.of(context).textTheme.headlineLarge), + SizedBox(height: size.height * 0.03), + _buildDropdownField(context, loginBloc, size), + const SizedBox(height: 20.0), + _buildEmailField(context, loginBloc), + const SizedBox(height: 20.0), + _buildPasswordField(context, loginBloc), + const SizedBox(height: 20), + _buildForgotPassword(context), + const SizedBox(height: 20), + _buildCheckbox(context, loginBloc, size), + const SizedBox(height: 20.0), + _buildSignInButton(context, loginBloc, size), + const SizedBox(height: 15.0), + _buildValidationMessage(loginBloc), + ], + ), + ), + ), + ); + } + + Widget _buildDropdownField(BuildContext context, AuthBloc loginBloc, Size size) { + final TextEditingController textEditingController = TextEditingController(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Country/Region", + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 10), + Container( + height: 50, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + width: size.width * 0.9, + child: DropdownButtonHideUnderline( + child: DropdownButton2( + style: TextStyle(color: Colors.black), + isExpanded: true, + hint: Text( + 'Select your region/country', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + ), + overflow: TextOverflow.ellipsis, + ), + items: loginBloc.regionList!.map((RegionModel region) { + return DropdownMenuItem( + value: region.id, // Use region.id as the value + child: Text( + region.name, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ); + }).toList(), + value: loginBloc.regionList!.any( + (region) => region.id == loginBloc.regionUuid, + ) + ? loginBloc.regionUuid + : null, + onChanged: (String? value) { + if (value != null) { + loginBloc.add(CheckEnableEvent()); + loginBloc.add(SelectRegionEvent(val: value)); + } + }, + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.symmetric(horizontal: 16), + height: 40, + width: double.infinity, + ), + dropdownStyleData: DropdownStyleData( + maxHeight: size.height * 0.70, + ), + menuItemStyleData: const MenuItemStyleData( + height: 40, + ), + dropdownSearchData: DropdownSearchData( + searchController: textEditingController, + searchInnerWidgetHeight: 50, + searchInnerWidget: Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: TextFormField( + style: const TextStyle(color: Colors.black), + controller: textEditingController, + decoration: textBoxDecoration()!.copyWith( + errorStyle: const TextStyle(height: 0), + contentPadding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 10, + ), + ), + ), + ), + searchMatchFn: (item, searchValue) { + // Use the item's child text (region name) for searching. + final regionName = (item.child as Text).data?.toLowerCase() ?? ''; + final search = searchValue.toLowerCase().trim(); + // Debugging print statement to ensure values are captured correctly. + // Return true if the region name contains the search term. + return regionName.contains(search); + }, + ), + onMenuStateChange: (isOpen) { + if (!isOpen) { + textEditingController.clear(); + } + }, + ), + ), + ), + ], + ); + } + + Widget _buildEmailField(BuildContext context, AuthBloc loginBloc) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Email", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + SizedBox( + child: TextFormField( + onChanged: (value) { + loginBloc.add(CheckEnableEvent()); + }, + validator: loginBloc.loginValidateEmail, + controller: loginBloc.loginEmailController, + decoration: textBoxDecoration()!.copyWith( + errorStyle: const TextStyle(height: 0), + hintText: 'Enter your email address', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor, fontWeight: FontWeight.w400)), + style: const TextStyle(color: Colors.black), + ), + ), + ], + ); + } + + Widget _buildPasswordField(BuildContext context, AuthBloc loginBloc) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Password", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + SizedBox( + child: TextFormField( + onChanged: (value) { + loginBloc.add(CheckEnableEvent()); + }, + validator: loginBloc.validatePassword, + obscureText: loginBloc.obscureText, + keyboardType: TextInputType.visiblePassword, + controller: loginBloc.loginPasswordController, + decoration: textBoxDecoration()!.copyWith( + hintText: 'At least 8 characters', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor, fontWeight: FontWeight.w400), + suffixIcon: IconButton( + onPressed: () { + loginBloc.add(PasswordVisibleEvent(newValue: loginBloc.obscureText)); + }, + icon: SizedBox( + child: SvgPicture.asset( + loginBloc.obscureText ? Assets.visiblePassword : Assets.invisiblePassword, + height: 15, + width: 15, + ), + ), + ), + errorStyle: const TextStyle(height: 0), + ), + style: const TextStyle(color: Colors.black), + ), + ), + ], + ); + } + + Widget _buildForgotPassword(BuildContext context) { + return SizedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const ForgetPasswordPage(), + )); + }, + child: Text( + "Forgot Password?", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400), + ), + ), + ], + ), + ); + } + + Widget _buildCheckbox(BuildContext context, AuthBloc loginBloc, Size size) { + return Row( + children: [ + Transform.scale( + scale: 1.2, + child: Checkbox( + fillColor: MaterialStateProperty.all(Colors.white), + activeColor: Colors.white, + value: loginBloc.isChecked, + checkColor: Colors.black, + shape: const CircleBorder(), + onChanged: (bool? newValue) { + loginBloc.add(CheckBoxEvent(newValue: newValue)); + }, + ), + ), + Expanded( + child: SizedBox( + child: RichText( + text: TextSpan( + text: 'Agree to ', + style: const TextStyle(color: Colors.white), + children: [ + TextSpan( + text: '(Terms of Service)', + style: const TextStyle(color: Colors.black), + recognizer: TapGestureRecognizer() + ..onTap = () { + loginBloc.launchURL('https://example.com/terms'); + }, + ), + TextSpan( + text: ' (Legal Statement)', + style: const TextStyle(color: Colors.black), + recognizer: TapGestureRecognizer() + ..onTap = () { + loginBloc.launchURL('https://example.com/legal'); + }, + ), + TextSpan( + text: ' (Privacy Statement)', + style: const TextStyle(color: Colors.black), + recognizer: TapGestureRecognizer() + ..onTap = () { + loginBloc.launchURL('https://example.com/privacy'); + }, + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildSignInButton(BuildContext context, AuthBloc loginBloc, Size size) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: size.width * 0.2, + child: DefaultButton( + enabled: loginBloc.checkValidate, + child: Text('Sign in', + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontSize: 14, + color: loginBloc.checkValidate + ? ColorsManager.whiteColors + : ColorsManager.whiteColors.withOpacity(0.2), + )), + onPressed: () { + if (loginBloc.loginFormKey.currentState!.validate()) { + loginBloc.add(LoginButtonPressed( + regionUuid: loginBloc.regionUuid, + username: loginBloc.loginEmailController.text, + password: loginBloc.loginPasswordController.text, + )); + } else { + loginBloc.add(ChangeValidateEvent()); + } + }, + ), + ), + ], + ); + } + + Widget _buildValidationMessage(AuthBloc loginBloc) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + child: Text( + loginBloc.validate, + style: const TextStyle(fontWeight: FontWeight.w700, color: ColorsManager.red), + ), + ) + ], + ); + } } diff --git a/lib/pages/common/access_device_table.dart b/lib/pages/common/access_device_table.dart new file mode 100644 index 00000000..86d4a6b3 --- /dev/null +++ b/lib/pages/common/access_device_table.dart @@ -0,0 +1,304 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class AccessDeviceTable extends StatefulWidget { + final List headers; + final String? tableName; + final List> data; + final BoxDecoration? headerDecoration; + final BoxDecoration? cellDecoration; + final Size size; + final bool withCheckBox; + final bool withSelectAll; + final bool isEmpty; + final void Function(bool?)? selectAll; + final void Function(int, bool, dynamic)? onRowSelected; + final List? initialSelectedIds; + final int uuidIndex; + const AccessDeviceTable({ + super.key, + required this.headers, + required this.data, + required this.size, + this.tableName, + required this.isEmpty, + required this.withCheckBox, + required this.withSelectAll, + this.headerDecoration, + this.cellDecoration, + this.selectAll, + this.onRowSelected, + this.initialSelectedIds, + required this.uuidIndex, + }); + + @override + _DynamicTableState createState() => _DynamicTableState(); +} + +class _DynamicTableState extends State { + late List _selected; + bool _selectAll = false; + + @override + void initState() { + super.initState(); + _initializeSelection(); + } + + @override + void didUpdateWidget(AccessDeviceTable oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data) { + _initializeSelection(); + } + } + + void _initializeSelection() { + if (widget.data.isEmpty) { + _selected = []; + _selectAll = false; + } else { + _selected = List.generate(widget.data.length, (index) { + // Check if the initialSelectedIds contains the deviceUuid + // uuidIndex is the index of the column containing the deviceUuid + final deviceUuid = widget.data[index][widget.uuidIndex]; + return widget.initialSelectedIds != null && + widget.initialSelectedIds!.contains(deviceUuid); + }); + _selectAll = _selected.every((element) => element == true); + } + } + + void _toggleRowSelection(int index) { + setState(() { + _selected[index] = !_selected[index]; + + if (widget.onRowSelected != null) { + widget.onRowSelected!(index, _selected[index], widget.data[index]); + } + }); + } + + void _toggleSelectAll(bool? value) { + setState(() { + _selectAll = value ?? false; + _selected = List.filled(widget.data.length, _selectAll); + if (widget.selectAll != null) { + widget.selectAll!(_selectAll); + } + }); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: widget.cellDecoration, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: widget.size.width, + child: Column( + children: [ + Container( + decoration: widget.headerDecoration ?? + BoxDecoration(color: Colors.grey[200]), + child: Row( + children: [ + if (widget.withCheckBox) _buildSelectAllCheckbox(), + ...widget.headers + .map((header) => _buildTableHeaderCell(header)), + ], + ), + ), + widget.isEmpty + ? Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox( + height: 15, + ), + Text( + // no password + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor), + ) + ], + ), + ], + ), + ], + ), + ) + : Expanded( + child: Container( + color: Colors.white, + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.data.length, + itemBuilder: (context, index) { + final row = widget.data[index]; + return Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox( + index, widget.size.height * 0.10), + ...row.map((cell) => _buildTableCell( + cell.toString(), + widget.size.height * 0.10)), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSelectAllCheckbox() { + return Container( + width: 50, + padding: const EdgeInsets.all(8.0), + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: ColorsManager.boxDivider), + ), + ), + child: Checkbox( + value: widget.data.isNotEmpty && + _selected.every((element) => element == true), + onChanged: widget.withSelectAll && widget.data.isNotEmpty + ? _toggleSelectAll + : null, + ), + ); + } + + Widget _buildRowCheckbox(int index, double size) { + return Container( + width: 50, + padding: const EdgeInsets.all(8.0), + height: size, + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, + ), + ), + ), + alignment: Alignment.centerLeft, + child: Center( + child: Checkbox( + value: _selected[index], + onChanged: (bool? value) { + _toggleRowSelection(index); + }, + ), + ), + ); + } + + Widget _buildTableHeaderCell(String title) { + return Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: ColorsManager.boxDivider), + ), + ), + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 13, + color: Color(0xFF999999), + ), + maxLines: 2, + ), + ), + ), + ); + } + + Widget _buildTableCell(String content, double size) { + bool isBatteryLevel = content.endsWith('%'); + double? batteryLevel; + + if (isBatteryLevel) { + batteryLevel = double.tryParse(content.replaceAll('%', '').trim()); + } + + Color? statusColor; + switch (content) { + case 'Effective': + statusColor = ColorsManager.textGreen; + break; + case 'Expired': + statusColor = ColorsManager.red; + break; + case 'To be effective': + statusColor = ColorsManager.yaGreen; + break; + case 'Online': + statusColor = ColorsManager.green; + break; + case 'Offline': + statusColor = ColorsManager.red; + break; + default: + statusColor = Colors.black; + } + + return Expanded( + child: Container( + height: size, + padding: const EdgeInsets.all(5.0), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, + ), + ), + ), + alignment: Alignment.centerLeft, + child: Text( + content, + style: TextStyle( + color: (batteryLevel != null && batteryLevel < 20) + ? ColorsManager.red + : (batteryLevel != null && batteryLevel > 20) + ? ColorsManager.green + : statusColor, + fontSize: 10, + fontWeight: FontWeight.w400), + maxLines: 2, + ), + ), + ); + } +} diff --git a/lib/pages/common/buttons/default_button.dart b/lib/pages/common/buttons/default_button.dart index 37320b26..2d901960 100644 --- a/lib/pages/common/buttons/default_button.dart +++ b/lib/pages/common/buttons/default_button.dart @@ -16,8 +16,9 @@ class DefaultButton extends StatelessWidget { this.foregroundColor, this.borderRadius, this.height = 40, - this.width = 140, this.padding, + this.borderColor, + this.elevation, }); final void Function()? onPressed; final Widget child; @@ -32,7 +33,8 @@ class DefaultButton extends StatelessWidget { final ButtonStyle? customButtonStyle; final Color? backgroundColor; final Color? foregroundColor; - final double? width; + final Color? borderColor; + final double? elevation; @override Widget build(BuildContext context) { @@ -42,38 +44,42 @@ class DefaultButton extends StatelessWidget { ? null : customButtonStyle ?? ButtonStyle( - fixedSize: WidgetStateProperty.all(Size(width ?? 50, height ?? 40)), // Set button height - textStyle: MaterialStateProperty.all( + textStyle: WidgetStateProperty.all( customTextStyle ?? Theme.of(context).textTheme.bodySmall!.copyWith( fontSize: 13, color: foregroundColor, fontWeight: FontWeight.normal), ), - foregroundColor: MaterialStateProperty.all( + foregroundColor: WidgetStateProperty.all( isSecondary ? Colors.black : enabled ? foregroundColor ?? Colors.white : Colors.black, ), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { return enabled ? backgroundColor ?? ColorsManager.primaryColor : Colors.black.withOpacity(0.2); }), - shape: WidgetStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius ?? 10), + side: BorderSide(color: borderColor ?? Colors.transparent), + borderRadius: BorderRadius.circular(borderRadius ?? 20), ), ), - padding: MaterialStateProperty.all( - EdgeInsets.all(padding ?? 10), - ), - minimumSize: MaterialStateProperty.all( + fixedSize: WidgetStateProperty.all( const Size.fromHeight(50), ), + padding: WidgetStateProperty.all( + EdgeInsets.all(padding ?? 10), + ), + minimumSize: WidgetStateProperty.all( + const Size.fromHeight(50), + ), + elevation: WidgetStateProperty.all(elevation ?? 0), ), child: SizedBox( height: height ?? 50, diff --git a/lib/pages/common/buttons/search_reset_buttons.dart b/lib/pages/common/buttons/search_reset_buttons.dart index a03b889a..7b63a485 100644 --- a/lib/pages/common/buttons/search_reset_buttons.dart +++ b/lib/pages/common/buttons/search_reset_buttons.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -23,14 +24,18 @@ class SearchResetButtons extends StatelessWidget { const SizedBox(height: 25), Center( child: Container( - height: 43, + height: 42, width: 100, decoration: containerDecoration, child: Center( child: DefaultButton( onPressed: onSearch, borderRadius: 9, - child: const Text('Search'), + child: Text( + 'Search', + style: context.textTheme.titleSmall! + .copyWith(color: Colors.white, fontSize: 12), + ), ), ), ), @@ -44,21 +49,19 @@ class SearchResetButtons extends StatelessWidget { const SizedBox(height: 25), Center( child: Container( - height: 43, + height: 42, width: 100, decoration: containerDecoration, child: Center( child: DefaultButton( backgroundColor: ColorsManager.whiteColors, borderRadius: 9, + onPressed: onReset, child: Text( 'Reset', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Colors.black), + style: context.textTheme.titleSmall! + .copyWith(color: Colors.black, fontSize: 12), ), - onPressed: onReset, ), ), ), diff --git a/lib/pages/common/curtain_toggle.dart b/lib/pages/common/curtain_toggle.dart new file mode 100644 index 00000000..7b1551c5 --- /dev/null +++ b/lib/pages/common/curtain_toggle.dart @@ -0,0 +1,73 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CurtainToggle extends StatelessWidget { + final bool value; + final String code; + final String deviceId; + final String label; + final Null Function(dynamic value) onChanged; + + const CurtainToggle({ + super.key, + required this.value, + required this.code, + required this.deviceId, + required this.label, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval( + child: Container( + color: ColorsManager.whiteColors, + child: SvgPicture.asset( + Assets.curtainIcon, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox( + width: 20, + ), + SizedBox( + height: 20, + width: 35, + child: CupertinoSwitch( + value: value, + activeColor: ColorsManager.dialogBlueTitle, + onChanged: onChanged, + ), + ), + ], + ), + const Spacer(), + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/common/custom_dialog.dart b/lib/pages/common/custom_dialog.dart index e75c1e90..a40ef10f 100644 --- a/lib/pages/common/custom_dialog.dart +++ b/lib/pages/common/custom_dialog.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; Future showCustomDialog({ required BuildContext context, @@ -13,7 +12,7 @@ Future showCustomDialog({ double? iconWidth, VoidCallback? onOkPressed, bool barrierDismissible = false, - required actions, + required List actions, }) { return showDialog( context: context, @@ -21,59 +20,43 @@ Future showCustomDialog({ builder: (BuildContext context) { final size = MediaQuery.of(context).size; return AlertDialog( - alignment: Alignment.center, - content: SizedBox( - height: dialogHeight ?? size.height * 0.15, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (iconPath != null) - SvgPicture.asset( - iconPath, - height: iconHeight ?? 35, - width: iconWidth ?? 35, - ), - if (title != null) + alignment: Alignment.center, + content: SizedBox( + height: dialogHeight ?? size.height * 0.15, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (iconPath != null) + SvgPicture.asset( + iconPath, + height: iconHeight ?? 35, + width: iconWidth ?? 35, + ), + if (title != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + title, + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith(fontSize: 20, fontWeight: FontWeight.w400, color: Colors.black), + ), + ), Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - title, - style: Theme.of(context).textTheme.headlineLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w400, - color: Colors.black), + message, + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.black), + textAlign: TextAlign.center, ), ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - message, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.black), - textAlign: TextAlign.center, - ), - ), - if(widget!=null) - Expanded(child:widget) - ], - ), - ), - actionsAlignment: MainAxisAlignment.center, - actions: [ - TextButton( - onPressed: onOkPressed ?? () => Navigator.of(context).pop(), - child: Text( - 'OK', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - fontSize: 16), + if (widget != null) Expanded(child: widget) + ], ), ), - ], - ); + actionsAlignment: MainAxisAlignment.center, + actions: actions); }, ); } diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index fdeff6b6..22baba36 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -1,31 +1,42 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class DynamicTable extends StatefulWidget { final List headers; + final String? tableName; final List> data; final BoxDecoration? headerDecoration; final BoxDecoration? cellDecoration; final Size size; final bool withCheckBox; + final bool withSelectAll; final bool isEmpty; final void Function(bool?)? selectAll; final void Function(int, bool, dynamic)? onRowSelected; final List? initialSelectedIds; + final int uuidIndex; + final Function(dynamic selectedRows)? onSelectionChanged; const DynamicTable({ super.key, required this.headers, required this.data, required this.size, + this.tableName, required this.isEmpty, required this.withCheckBox, + required this.withSelectAll, this.headerDecoration, this.cellDecoration, this.selectAll, this.onRowSelected, this.initialSelectedIds, + required this.uuidIndex, + this.onSelectionChanged, }); @override @@ -33,98 +44,142 @@ class DynamicTable extends StatefulWidget { } class _DynamicTableState extends State { - late List _selected; + late List _selectedRows; + bool _selectAll = false; + final ScrollController _verticalScrollController = ScrollController(); + final ScrollController _horizontalScrollController = ScrollController(); @override void initState() { super.initState(); - _selected = List.generate(widget.data.length, (index) { - return widget.initialSelectedIds != null && - widget.initialSelectedIds!.contains(widget.data[index][1]); - }); + _initializeSelection(); + } + + @override + void didUpdateWidget(DynamicTable oldWidget) { + super.didUpdateWidget(oldWidget); + if (!_compareListOfLists(oldWidget.data, widget.data)) { + _initializeSelection(); + } + } + + bool _compareListOfLists(List> oldList, List> newList) { + // Check if the old and new lists are the same + if (oldList.length != newList.length) return false; + + for (int i = 0; i < oldList.length; i++) { + if (oldList[i].length != newList[i].length) return false; + + for (int j = 0; j < oldList[i].length; j++) { + if (oldList[i][j] != newList[i][j]) return false; + } + } + + return true; + } + + void _initializeSelection() { + _selectedRows = List.filled(widget.data.length, false); + _selectAll = false; } void _toggleRowSelection(int index) { setState(() { - _selected[index] = !_selected[index]; - - if (widget.onRowSelected != null) { - widget.onRowSelected!(index, _selected[index], widget.data[index]); - } + _selectedRows[index] = !_selectedRows[index]; + _selectAll = _selectedRows.every((isSelected) => isSelected); }); + widget.onSelectionChanged?.call(_selectedRows); + context.read().add(UpdateSelection(_selectedRows)); + } + + void _toggleSelectAll(bool? value) { + setState(() { + _selectAll = value ?? false; + _selectedRows = List.filled(widget.data.length, _selectAll); + }); + widget.onSelectionChanged?.call(_selectedRows); + context.read().add(UpdateSelection(_selectedRows)); } @override Widget build(BuildContext context) { return Container( decoration: widget.cellDecoration, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - width: widget.size.width, - child: Column( - children: [ - Container( - decoration: widget.headerDecoration ?? BoxDecoration(color: Colors.grey[200]), - child: Row( + child: Scrollbar( + controller: _verticalScrollController, + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontalScrollController, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _verticalScrollController, + child: SingleChildScrollView( + controller: _horizontalScrollController, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: widget.size.width, + child: Column( children: [ - if (widget.withCheckBox) _buildSelectAllCheckbox(), - ...widget.headers.map((header) => _buildTableHeaderCell(header)).toList(), - ], - ), - ), - widget.isEmpty - ? Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + Container( + decoration: widget.headerDecoration ?? + const BoxDecoration( + color: ColorsManager.boxColor, + ), + child: Row( children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, + if (widget.withCheckBox) _buildSelectAllCheckbox(), + ...List.generate(widget.headers.length, (index) { + return _buildTableHeaderCell(widget.headers[index], index); + }) + //...widget.headers.map((header) => _buildTableHeaderCell(header)), + ], + ), + ), + widget.isEmpty + ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.asset(Assets.emptyTable), - const SizedBox( - height: 15, + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox( + height: 15, + ), + Text( + widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor), + ) + ], ), - Text( - 'No Devices', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: ColorsManager.grayColor), - ) ], ), ], + ) + : Column( + children: List.generate(widget.data.length, (index) { + final row = widget.data[index]; + return Row( + children: [ + if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08), + ...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)), + ], + ); + }), ), - ], - ), - ) - : Expanded( - child: Container( - color: Colors.white, - child: ListView.builder( - shrinkWrap: true, - itemCount: widget.data.length, - itemBuilder: (context, index) { - final row = widget.data[index]; - return Row( - children: [ - if (widget.withCheckBox) - _buildRowCheckbox(index, widget.size.height * 0.10), - ...row - .map((cell) => - _buildTableCell(cell.toString(), widget.size.height * 0.10)) - .toList(), - ], - ); - }, - ), - ), - ), - ], + ], + ), + ), + ), ), ), ), @@ -134,15 +189,14 @@ class _DynamicTableState extends State { Widget _buildSelectAllCheckbox() { return Container( width: 50, - padding: const EdgeInsets.all(8.0), decoration: const BoxDecoration( border: Border.symmetric( vertical: BorderSide(color: ColorsManager.boxDivider), ), ), child: Checkbox( - value: _selected.every((element) => element == true), - onChanged: null, + value: _selectAll, + onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, ), ); } @@ -159,11 +213,12 @@ class _DynamicTableState extends State { width: 1.0, ), ), + color: ColorsManager.whiteColors, ), alignment: Alignment.centerLeft, child: Center( child: Checkbox( - value: _selected[index], + value: _selectedRows[index], onChanged: (bool? value) { _toggleRowSelection(index); }, @@ -172,7 +227,7 @@ class _DynamicTableState extends State { ); } - Widget _buildTableHeaderCell(String title) { + Widget _buildTableHeaderCell(String title, int index) { return Expanded( child: Container( decoration: const BoxDecoration( @@ -180,16 +235,18 @@ class _DynamicTableState extends State { vertical: BorderSide(color: ColorsManager.boxDivider), ), ), + constraints: const BoxConstraints.expand(height: 40), alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), child: Text( title, - style: const TextStyle( + style: context.textTheme.titleSmall!.copyWith( + color: ColorsManager.grayColor, + fontSize: 12, fontWeight: FontWeight.w400, - fontSize: 13, - color: Color(0xFF999999), ), + maxLines: 2, ), ), ), @@ -222,7 +279,7 @@ class _DynamicTableState extends State { statusColor = ColorsManager.red; break; default: - statusColor = Colors.black; // Default color + statusColor = Colors.black; } return Expanded( @@ -236,16 +293,20 @@ class _DynamicTableState extends State { width: 1.0, ), ), + color: Colors.white, ), alignment: Alignment.centerLeft, child: Text( content, style: TextStyle( - color: batteryLevel != null && batteryLevel < 20 + color: (batteryLevel != null && batteryLevel < 20) ? ColorsManager.red - : statusColor, // Use the passed color or default to black - fontSize: 10, + : (batteryLevel != null && batteryLevel > 20) + ? ColorsManager.green + : statusColor, + fontSize: 13, fontWeight: FontWeight.w400), + maxLines: 2, ), ), ); diff --git a/lib/pages/common/date_time_widget.dart b/lib/pages/common/date_time_widget.dart index 0b8fa0da..8dbcc9aa 100644 --- a/lib/pages/common/date_time_widget.dart +++ b/lib/pages/common/date_time_widget.dart @@ -36,7 +36,10 @@ class DateTimeWebWidget extends StatelessWidget { if (isRequired) Text( '* ', - style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.red), + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), ), Text( title, @@ -51,8 +54,9 @@ class DateTimeWebWidget extends StatelessWidget { height: 8, ), Container( - height: size.height * 0.055, - padding: const EdgeInsets.only(top: 10, bottom: 10, right: 30, left: 10), + // height: size.height * 0.056, + padding: + const EdgeInsets.only(top: 10, bottom: 10, right: 30, left: 10), decoration: containerDecoration, child: FittedBox( child: Column( @@ -65,16 +69,22 @@ class DateTimeWebWidget extends StatelessWidget { child: FittedBox( child: Text( firstString, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - fontSize: 12, - fontWeight: FontWeight.w400), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 12, + fontWeight: FontWeight.w400), ), )), const SizedBox( width: 30, ), - const Icon(Icons.arrow_right_alt), + const Icon( + Icons.arrow_right_alt, + color: ColorsManager.grayColor, + ), const SizedBox( width: 30, ), @@ -83,10 +93,13 @@ class DateTimeWebWidget extends StatelessWidget { child: FittedBox( child: Text( secondString, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - fontSize: 12, - fontWeight: FontWeight.w400), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 12, + fontWeight: FontWeight.w400), ), )), const SizedBox( diff --git a/lib/pages/common/filter/filter_widget.dart b/lib/pages/common/filter/filter_widget.dart index d6cfcc7e..1af23045 100644 --- a/lib/pages/common/filter/filter_widget.dart +++ b/lib/pages/common/filter/filter_widget.dart @@ -20,7 +20,7 @@ class FilterWidget extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: containerDecoration, - height: size.height * 0.05, + height: 40, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: tabs.length, diff --git a/lib/pages/common/hour_picker_dialog.dart b/lib/pages/common/hour_picker_dialog.dart index 2c89b710..01a87720 100644 --- a/lib/pages/common/hour_picker_dialog.dart +++ b/lib/pages/common/hour_picker_dialog.dart @@ -1,7 +1,10 @@ + + import 'package:flutter/material.dart'; class HourPickerDialog extends StatefulWidget { final TimeOfDay initialTime; + const HourPickerDialog({super.key, required this.initialTime}); @override @@ -9,70 +12,50 @@ class HourPickerDialog extends StatefulWidget { } class _HourPickerDialogState extends State { - late int _selectedHour; - bool _isPm = false; + late String selectedHour; @override void initState() { super.initState(); - _selectedHour = widget.initialTime.hour > 12 - ? widget.initialTime.hour - 12 - : widget.initialTime.hour; - _isPm = widget.initialTime.period == DayPeriod.pm; + // Initialize the selectedHour with the initial time passed to the dialog + selectedHour = widget.initialTime.hour.toString().padLeft(2, '0') + ':00'; } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Select Hour'), - content: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DropdownButton( - value: _selectedHour, - items: List.generate(12, (index) { - int displayHour = index + 1; - return DropdownMenuItem( - value: displayHour, - child: Text(displayHour.toString()), - ); - }), - onChanged: (value) { - setState(() { - _selectedHour = value!; - }); - }, - ), - SizedBox(width: 16.0), - DropdownButton( - value: _isPm, - items: const [ - DropdownMenuItem( - value: false, - child: Text('AM'), - ), - DropdownMenuItem( - value: true, - child: Text('PM'), - ), - ], - onChanged: (value) { - setState(() { - _isPm = value!; - }); - }, - ), - ], + content: DropdownButton( + value: selectedHour, // Show the currently selected hour + items: List.generate(24, (index) { + String hour = index.toString().padLeft(2, '0'); + return DropdownMenuItem( + value: '$hour:00', + child: Text('$hour:00'), + ); + }), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + selectedHour = newValue; // Update the selected hour without closing the dialog + }); + } + }, ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(null), + onPressed: () => Navigator.of(context).pop(null), // Close the dialog without selection child: const Text('Cancel'), ), TextButton( onPressed: () { - int hour = _isPm ? _selectedHour + 12 : _selectedHour; - Navigator.of(context).pop(TimeOfDay(hour: hour, minute: 0)); + // Close the dialog and return the selected time + Navigator.of(context).pop( + TimeOfDay( + hour: int.parse(selectedHour.split(':')[0]), + minute: 0, + ), + ); }, child: const Text('OK'), ), @@ -86,6 +69,7 @@ Future showHourPicker({ required TimeOfDay initialTime, }) { return showDialog( + barrierDismissible: false, context: context, builder: (context) => HourPickerDialog(initialTime: initialTime), ); diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index 587a1f4f..b695da4a 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -1,21 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/core/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; class StatefulTextField extends StatefulWidget { - const StatefulTextField({ - super.key, - required this.title, - this.hintText = 'Please enter', - required this.width, - this.elevation = 0, - required this.controller, // Add the controller - }); + const StatefulTextField( + {super.key, + required this.title, + this.hintText = 'Please enter', + required this.width, + this.elevation = 0, + required this.controller, + this.onSubmitted}); final String title; final String hintText; final double width; final double elevation; - final TextEditingController controller; + final TextEditingController controller; + final Function? onSubmitted; @override State createState() => _StatefulTextFieldState(); @@ -24,31 +26,34 @@ class StatefulTextField extends StatefulWidget { class _StatefulTextFieldState extends State { @override Widget build(BuildContext context) { - return CustomTextField( - title: widget.title, - controller: widget.controller, - hintText: widget.hintText, - width: widget.width, - elevation: widget.elevation, + return Container( + child: CustomTextField( + title: widget.title, + controller: widget.controller, + hintText: widget.hintText, + width: widget.width, + elevation: widget.elevation, + onSubmittedFun: widget.onSubmitted), ); } } class CustomTextField extends StatelessWidget { - const CustomTextField({ - super.key, - required this.title, - required this.controller, - this.hintText = 'Please enter', - required this.width, - this.elevation = 0, - }); + const CustomTextField( + {super.key, + required this.title, + required this.controller, + this.hintText = 'Please enter', + required this.width, + this.elevation = 0, + this.onSubmittedFun}); final String title; final TextEditingController controller; final String hintText; final double width; final double elevation; + final Function? onSubmittedFun; @override Widget build(BuildContext context) { @@ -59,6 +64,7 @@ class CustomTextField extends StatelessWidget { Text( title, style: context.textTheme.bodyMedium!.copyWith( + fontSize: 13, fontWeight: FontWeight.w600, color: const Color(0xff000000), ), @@ -69,19 +75,26 @@ class CustomTextField extends StatelessWidget { borderRadius: BorderRadius.circular(8), child: Container( width: width, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), + height: 45, + decoration: containerDecoration, + + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(8), + // ), child: TextFormField( controller: controller, style: const TextStyle(color: Colors.black), decoration: InputDecoration( hintText: hintText, + hintStyle: const TextStyle(fontSize: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: InputBorder.none, ), + onFieldSubmitted: (_) { + onSubmittedFun!(); + }, ), ), ), diff --git a/lib/pages/common/text_field/custom_web_textfield.dart b/lib/pages/common/text_field/custom_web_textfield.dart index 6fe0dc49..630e334b 100644 --- a/lib/pages/common/text_field/custom_web_textfield.dart +++ b/lib/pages/common/text_field/custom_web_textfield.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class CustomWebTextField extends StatelessWidget { @@ -11,6 +12,8 @@ class CustomWebTextField extends StatelessWidget { this.description, this.validator, this.hintText, + this.height, + this.onSubmitted, }); final bool isRequired; @@ -19,6 +22,8 @@ class CustomWebTextField extends StatelessWidget { final TextEditingController? controller; final String? Function(String?)? validator; final String? hintText; + final double? height; + final ValueChanged? onSubmitted; @override Widget build(BuildContext context) { @@ -29,9 +34,9 @@ class CustomWebTextField extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (isRequired) - Row( - children: [ + Row( + children: [ + if (isRequired) Text( '* ', style: Theme.of(context) @@ -39,15 +44,15 @@ class CustomWebTextField extends StatelessWidget { .bodyMedium! .copyWith(color: Colors.red), ), - Text( - textFieldName, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Colors.black, fontSize: 13), - ), - ], - ), + Text( + textFieldName, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13), + ), + ], + ), const SizedBox( width: 10, ), @@ -66,24 +71,18 @@ class CustomWebTextField extends StatelessWidget { height: 7, ), Container( - decoration: containerDecoration - .copyWith(color: const Color(0xFFF5F6F7), boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 3, - offset: const Offset(1, 1), // changes position of shadow - ), - ]), + height: height ?? 35, + decoration: containerDecoration, child: TextFormField( validator: validator, controller: controller, style: const TextStyle(color: Colors.black), decoration: textBoxDecoration()!.copyWith( - errorStyle: - const TextStyle(height: 0), // Hide the error text space - + errorStyle: const TextStyle(height: 0), + hintStyle: context.textTheme.titleSmall! + .copyWith(color: Colors.grey, fontSize: 12), hintText: hintText ?? 'Please enter'), + onFieldSubmitted: onSubmitted, ), ), ], diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 5d147439..7c6ee628 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -14,12 +14,15 @@ class AcBloc extends Bloc { Timer? _timer; AcBloc({required this.deviceId}) : super(AcsInitialState()) { - on(_onFetchAcStatus); - on(_onAcControl); + on(_onFetchAcStatus); + on(_onFetchAcBatchStatus); + on(_onAcControl); + on(_onAcBatchControl); + on(_onFactoryReset); } FutureOr _onFetchAcStatus( - AcFetchDeviceStatus event, Emitter emit) async { + AcFetchDeviceStatusEvent event, Emitter emit) async { emit(AcsLoadingState()); try { final status = @@ -31,7 +34,8 @@ class AcBloc extends Bloc { } } - FutureOr _onAcControl(AcControl event, Emitter emit) async { + FutureOr _onAcControl( + AcControlEvent event, Emitter emit) async { final oldValue = _getValueByCode(event.code); _updateLocalValue(event.code, event.value, emit); @@ -39,6 +43,7 @@ class AcBloc extends Bloc { emit(ACStatusLoaded(deviceStatus)); await _runDebounce( + isBatch: false, deviceId: event.deviceId, code: event.code, value: event.value, @@ -48,27 +53,43 @@ class AcBloc extends Bloc { } Future _runDebounce({ - required String deviceId, + required dynamic deviceId, required String code, required dynamic value, required dynamic oldValue, required Emitter emit, + required bool isBatch, }) async { + late String id; + + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + if (_timer != null) { _timer!.cancel(); } _timer = Timer(const Duration(seconds: 1), () async { try { - final response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + if (!response) { - _revertValueAndEmit(deviceId, code, oldValue, emit); + _revertValueAndEmit(id, code, oldValue, emit); } } catch (e) { if (e is DioException && e.response != null) { debugPrint('Error response: ${e.response?.data}'); } - _revertValueAndEmit(deviceId, code, oldValue, emit); + _revertValueAndEmit(id, code, oldValue, emit); } }); } @@ -77,7 +98,6 @@ class AcBloc extends Bloc { String deviceId, String code, dynamic oldValue, Emitter emit) { _updateLocalValue(code, oldValue, emit); emit(ACStatusLoaded(deviceStatus)); - emit(const AcsFailedState(error: 'Failed to control the device.')); } void _updateLocalValue(String code, dynamic value, Emitter emit) { @@ -133,4 +153,54 @@ class AcBloc extends Bloc { return null; } } + + FutureOr _onFetchAcBatchStatus( + AcFetchBatchStatusEvent event, Emitter emit) async { + emit(AcsLoadingState()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + AcStatusModel.fromJson(event.devicesIds.first, status.status); + emit(ACStatusLoaded(deviceStatus)); + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } + } + + FutureOr _onAcBatchControl( + AcBatchControlEvent event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value, emit); + + emit(ACStatusLoaded(deviceStatus)); + + await _runDebounce( + isBatch: true, + deviceId: event.devicesIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + ); + } + + FutureOr _onFactoryReset( + AcFactoryResetEvent event, Emitter emit) async { + emit(AcsLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryResetModel, + event.deviceId, + ); + if (!response) { + emit(const AcsFailedState(error: 'Failed')); + } else { + add(AcFetchDeviceStatusEvent(event.deviceId)); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } + } } diff --git a/lib/pages/device_managment/ac/bloc/ac_event.dart b/lib/pages/device_managment/ac/bloc/ac_event.dart index 400c8136..8d49df96 100644 --- a/lib/pages/device_managment/ac/bloc/ac_event.dart +++ b/lib/pages/device_managment/ac/bloc/ac_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; sealed class AcsEvent extends Equatable { const AcsEvent(); @@ -7,21 +8,30 @@ sealed class AcsEvent extends Equatable { List get props => []; } -class AcFetchDeviceStatus extends AcsEvent { +class AcFetchDeviceStatusEvent extends AcsEvent { final String deviceId; - const AcFetchDeviceStatus(this.deviceId); + const AcFetchDeviceStatusEvent(this.deviceId); @override List get props => [deviceId]; } -class AcControl extends AcsEvent { +class AcFetchBatchStatusEvent extends AcsEvent { + final List devicesIds; + + const AcFetchBatchStatusEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class AcControlEvent extends AcsEvent { final String deviceId; final String code; final dynamic value; - const AcControl({ + const AcControlEvent({ required this.deviceId, required this.code, required this.value, @@ -30,3 +40,31 @@ class AcControl extends AcsEvent { @override List get props => [deviceId, code, value]; } + +class AcBatchControlEvent extends AcsEvent { + final List devicesIds; + final String code; + final dynamic value; + + const AcBatchControlEvent({ + required this.devicesIds, + required this.code, + required this.value, + }); + + @override + List get props => [devicesIds, code, value]; +} + +class AcFactoryResetEvent extends AcsEvent { + final String deviceId; + final FactoryResetModel factoryResetModel; + + const AcFactoryResetEvent({ + required this.deviceId, + required this.factoryResetModel, + }); + + @override + List get props => [deviceId, factoryResetModel]; +} diff --git a/lib/pages/device_managment/ac/bloc/ac_state.dart b/lib/pages/device_managment/ac/bloc/ac_state.dart index 9a3b07f9..dfd12e6d 100644 --- a/lib/pages/device_managment/ac/bloc/ac_state.dart +++ b/lib/pages/device_managment/ac/bloc/ac_state.dart @@ -22,6 +22,16 @@ class ACStatusLoaded extends AcsState { List get props => [status, timestamp]; } +class AcBatchStatusLoaded extends AcsState { + final AcStatusModel status; + final DateTime timestamp; + + AcBatchStatusLoaded(this.status) : timestamp = DateTime.now(); + + @override + List get props => [status, timestamp]; +} + class AcsFailedState extends AcsState { final String error; diff --git a/lib/pages/device_managment/ac/model/ac_model.dart b/lib/pages/device_managment/ac/model/ac_model.dart index 621b9326..2803e51e 100644 --- a/lib/pages/device_managment/ac/model/ac_model.dart +++ b/lib/pages/device_managment/ac/model/ac_model.dart @@ -40,16 +40,16 @@ class AcStatusModel { acSwitch = status.value ?? false; break; case 'mode': - mode = status.value ?? 'cold'; // default to 'cold' if null + mode = status.value ?? 'cold'; break; case 'temp_set': - tempSet = status.value ?? 210; // default value if null + tempSet = status.value ?? 210; break; case 'temp_current': - currentTemp = status.value ?? 210; // default value if null + currentTemp = status.value ?? 210; break; case 'level': - fanSpeeds = status.value ?? 'low'; // default value if null + fanSpeeds = status.value ?? 'low'; break; case 'child_lock': childLock = status.value ?? false; diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart new file mode 100644 index 00000000..2da394c7 --- /dev/null +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart'; +import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart'; +import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout { + const AcDeviceBatchControlView({super.key, required this.devicesIds}); + + final List devicesIds; + + @override + Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return BlocProvider( + create: (context) => AcBloc(deviceId: devicesIds.first)..add(AcFetchBatchStatusEvent(devicesIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is ACStatusLoaded) { + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + deviceId: devicesIds.first, + code: 'switch', + value: state.status.acSwitch, + label: 'ThermoState', + icon: Assets.ac, + onChange: (value) { + context.read().add(AcBatchControlEvent( + devicesIds: devicesIds, + code: 'switch', + value: value, + )); + }, + ), + BatchCurrentTemp( + currentTemp: state.status.currentTemp, + tempSet: state.status.tempSet, + code: 'temp_set', + devicesIds: devicesIds, + isBatch: true, + ), + BatchAcMode( + value: state.status.acMode, + code: 'mode', + devicesIds: devicesIds, + ), + BatchFanSpeedControl( + value: state.status.acFanSpeed, + code: 'level', + devicesIds: devicesIds, + ), + ToggleWidget( + label: '', + labelWidget: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.remove, + size: 28, + color: ColorsManager.greyColor, + ), + ), + Text( + '06', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'h', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor), + ), + Text( + '30', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text('m', style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor)), + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.add, + size: 28, + color: ColorsManager.greyColor, + ), + ), + ], + ), + value: false, + code: 'ac_schedule', + deviceId: devicesIds.first, + icon: Assets.acSchedule, + onChange: (value) {}, + ), + ToggleWidget( + deviceId: devicesIds.first, + code: 'child_lock', + value: state.status.childLock, + label: 'Child Lock', + icon: state.status.childLock ? Assets.acLock : Assets.unlock, + onChange: (value) { + context.read().add(AcBatchControlEvent( + devicesIds: devicesIds, + code: 'child_lock', + value: value, + )); + }, + ), + FirmwareUpdateWidget(deviceId: devicesIds.first, version: 5), + FactoryResetWidget( + callFactoryReset: () { + context.read().add(AcFactoryResetEvent( + deviceId: state.status.uuid, + factoryResetModel: FactoryResetModel(devicesUuid: devicesIds), + )); + }, + ), + ], + ); + } else if (state is AcsLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else { + return const Center(child: Text('Error fetching status')); + } + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/ac_device_control.dart b/lib/pages/device_managment/ac/view/ac_device_control.dart index 5c49ecfa..5197d722 100644 --- a/lib/pages/device_managment/ac/view/ac_device_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_control.dart @@ -4,25 +4,27 @@ import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/ac_mode.dart'; -import 'package:syncrow_web/pages/device_managment/ac/view/control_list/ac_toggle.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/fan_speed.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class AcDeviceControl extends StatelessWidget with HelperResponsiveLayout { - const AcDeviceControl({super.key, required this.device}); +class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { + const AcDeviceControlsView({super.key, required this.device}); final AllDevicesModel device; @override Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => AcBloc(deviceId: device.uuid!) - ..add(AcFetchDeviceStatus(device.uuid!)), + create: (context) => AcBloc(deviceId: device.uuid!)..add(AcFetchDeviceStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { if (state is ACStatusLoaded) { @@ -31,20 +33,31 @@ class AcDeviceControl extends StatelessWidget with HelperResponsiveLayout { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isLarge + crossAxisCount: isLarge || isExtraLarge ? 3 : isMedium ? 2 : 1, mainAxisExtent: 140, crossAxisSpacing: 12, - mainAxisSpacing: 12, + mainAxisSpacing: 16, ), children: [ - AcToggle( + ToggleWidget( + label: 'Thermostat', value: state.status.acSwitch, code: 'switch', deviceId: device.uuid!, + icon: Assets.ac, + onChange: (value) { + context.read().add( + AcControlEvent( + deviceId: device.uuid!, + code: 'switch', + value: value, + ), + ); + }, ), CurrentTemp( currentTemp: state.status.currentTemp, @@ -62,12 +75,71 @@ class AcDeviceControl extends StatelessWidget with HelperResponsiveLayout { code: 'level', deviceId: device.uuid!, ), - AcToggle( - value: state.status.childLock, - code: 'child_lock', + ToggleWidget( + label: '', + labelWidget: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + padding: const EdgeInsets.all(0), + onPressed: () {}, + icon: const Icon( + Icons.remove, + size: 28, + color: ColorsManager.greyColor, + ), + ), + Text( + '06', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'h', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor), + ), + Text( + '30', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text('m', style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor)), + IconButton( + padding: const EdgeInsets.all(0), + onPressed: () {}, + icon: const Icon( + Icons.add, + size: 28, + color: ColorsManager.greyColor, + ), + ), + ], + ), + value: false, + code: 'ac_schedule', deviceId: device.uuid!, - description: 'Child Lock', - icon: Assets.childLock, + icon: Assets.acSchedule, + onChange: (value) {}, + ), + ToggleWidget( + deviceId: device.uuid!, + code: 'child_lock', + value: state.status.childLock, + label: 'Lock', + icon: state.status.childLock ? Assets.acLock : Assets.unlock, + onChange: (value) { + context.read().add( + AcControlEvent( + deviceId: device.uuid!, + code: 'child_lock', + value: value, + ), + ); + }, ), ], ); diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart new file mode 100644 index 00000000..60d48256 --- /dev/null +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BatchAcMode extends StatelessWidget { + const BatchAcMode({ + super.key, + required this.value, + required this.code, + required this.devicesIds, + }); + + final TempModes value; + final String code; + final List devicesIds; + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildIconContainer(context, TempModes.cold, Assets.freezing, value == TempModes.cold), + _buildIconContainer(context, TempModes.hot, Assets.acSun, value == TempModes.hot), + _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, value == TempModes.wind), + ], + ), + ); + } + + Widget _buildIconContainer(BuildContext context, TempModes mode, String assetPath, bool isSelected) { + return Flexible( + child: GestureDetector( + onTap: () { + context.read().add( + AcBatchControlEvent( + devicesIds: devicesIds, + code: code, + value: mode.name, + ), + ); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + border: Border.all( + color: isSelected ? Colors.blue : Colors.transparent, + width: 2.0, + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(4), + child: ClipOval( + child: SvgPicture.asset( + assetPath, + fit: BoxFit.contain, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart new file mode 100644 index 00000000..be7441df --- /dev/null +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/shared/celciuse_symbol.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/shared/increament_decreament.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class BatchCurrentTemp extends StatefulWidget { + const BatchCurrentTemp({ + super.key, + required this.code, + required this.devicesIds, + required this.currentTemp, + required this.tempSet, + this.isBatch, + }); + + final String code; + final List devicesIds; + final int currentTemp; + final int tempSet; + final bool? isBatch; + + @override + State createState() => _CurrentTempState(); +} + +class _CurrentTempState extends State { + late double _adjustedValue; + Timer? _debounce; + + @override + void initState() { + super.initState(); + _adjustedValue = _initialAdjustedValue(widget.tempSet); + } + + double _initialAdjustedValue(dynamic value) { + if (value is int || value is double) { + double doubleValue = value.toDouble(); + return doubleValue > 99 ? doubleValue / 10 : doubleValue; + } else { + throw ArgumentError('Invalid value type: Expected int or double'); + } + } + + void _onValueChanged(double newValue) { + if (_debounce?.isActive ?? false) { + _debounce?.cancel(); + } + _debounce = Timer(const Duration(milliseconds: 500), () { + context.read().add( + AcBatchControlEvent( + devicesIds: widget.devicesIds, + code: widget.code, + value: (newValue * 10).toInt(), + ), + ); + }); + } + + @override + void dispose() { + _debounce?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.isBatch == true + ? Text( + 'Set Temperature', + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.grey), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Temperature', + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.grey), + ), + const SizedBox( + height: 5, + ), + Row( + children: [ + Text( + (widget.currentTemp > 99 ? widget.currentTemp / 10 : widget.currentTemp).toString(), + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.grey), + ), + const CelsiusSymbol( + color: Colors.grey, + ) + ], + ), + ], + ), + const Spacer(), + IncrementDecrementWidget( + value: _adjustedValue.toString(), + description: '°C', + descriptionColor: ColorsManager.dialogBlueTitle, + onIncrement: () { + if (_adjustedValue < 30) { + setState(() { + _adjustedValue = _adjustedValue + 0.5; + }); + _onValueChanged(_adjustedValue); + } + }, + onDecrement: () { + if (_adjustedValue > 20) { + setState(() { + _adjustedValue = _adjustedValue - 0.5; + }); + _onValueChanged(_adjustedValue); + } + }), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart new file mode 100644 index 00000000..ba49047a --- /dev/null +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BatchFanSpeedControl extends StatelessWidget { + const BatchFanSpeedControl({ + super.key, + required this.value, + required this.code, + required this.devicesIds, + }); + + final FanSpeeds value; + final String code; + final List devicesIds; + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + padding: 8, + child: Column( + children: [ + Wrap( + runSpacing: 8, + spacing: 8, + children: [ + _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, value == FanSpeeds.auto), + _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, value == FanSpeeds.low), + ], + ), + const SizedBox(height: 8), + Wrap( + runSpacing: 8, + spacing: 8, + children: [ + _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, value == FanSpeeds.middle), + _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, value == FanSpeeds.high), + ], + ) + ], + ), + ); + } + + Widget _buildIconContainer(BuildContext context, FanSpeeds speed, String assetPath, bool isSelected) { + return GestureDetector( + onTap: () { + context.read().add( + AcBatchControlEvent( + devicesIds: devicesIds, + code: code, + value: speed.name, + ), + ); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + border: Border.all( + color: isSelected ? Colors.blue : Colors.transparent, + width: 2.0, + ), + ), + padding: const EdgeInsets.all(8), + child: ClipOval( + child: SvgPicture.asset( + assetPath, + fit: BoxFit.contain, + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/control_list/ac_mode.dart b/lib/pages/device_managment/ac/view/control_list/ac_mode.dart index 0e0cd276..c6ffc052 100644 --- a/lib/pages/device_managment/ac/view/control_list/ac_mode.dart +++ b/lib/pages/device_managment/ac/view/control_list/ac_mode.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class AcMode extends StatelessWidget { const AcMode({ @@ -21,35 +22,25 @@ class AcMode extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ColorsManager.greyColor.withOpacity(0.2), - border: Border.all(color: ColorsManager.boxDivider), - ), - padding: const EdgeInsets.all(16), + return DeviceControlsContainer( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildIconContainer(context, TempModes.cold, Assets.freezing, - value == TempModes.cold), - _buildIconContainer( - context, TempModes.hot, Assets.acSun, value == TempModes.hot), - _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, - value == TempModes.wind), + _buildIconContainer(context, TempModes.cold, Assets.freezing, value == TempModes.cold), + _buildIconContainer(context, TempModes.hot, Assets.acSun, value == TempModes.hot), + _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, value == TempModes.wind), ], ), ); } - Widget _buildIconContainer( - BuildContext context, TempModes mode, String assetPath, bool isSelected) { + Widget _buildIconContainer(BuildContext context, TempModes mode, String assetPath, bool isSelected) { return Flexible( child: GestureDetector( onTap: () { context.read().add( - AcControl( + AcControlEvent( deviceId: deviceId, code: code, value: mode.name, diff --git a/lib/pages/device_managment/ac/view/control_list/ac_toggle.dart b/lib/pages/device_managment/ac/view/control_list/ac_toggle.dart index 53f79761..4e81ec09 100644 --- a/lib/pages/device_managment/ac/view/control_list/ac_toggle.dart +++ b/lib/pages/device_managment/ac/view/control_list/ac_toggle.dart @@ -1,9 +1,9 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -25,13 +25,7 @@ class AcToggle extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ColorsManager.greyColor.withOpacity(0.2), - border: Border.all(color: ColorsManager.boxDivider), - ), - padding: const EdgeInsets.all(16), + return DeviceControlsContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -39,16 +33,21 @@ class AcToggle extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipOval( - child: Container( - color: ColorsManager.whiteColors, - child: SvgPicture.asset( - icon ?? Assets.acDevice, - width: 60, - height: 60, - fit: BoxFit.cover, + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, ), - )), + padding: const EdgeInsets.all(8), + child: ClipOval( + child: SvgPicture.asset( + icon ?? Assets.lightPulp, + fit: BoxFit.contain, + ), + ), + ), SizedBox( height: 20, width: 35, @@ -57,7 +56,7 @@ class AcToggle extends StatelessWidget { value: value, onChanged: (newValue) { context.read().add( - AcControl( + AcControlEvent( deviceId: deviceId, code: code, value: newValue, diff --git a/lib/pages/device_managment/ac/view/control_list/current_temp.dart b/lib/pages/device_managment/ac/view/control_list/current_temp.dart index fc00479b..7618846c 100644 --- a/lib/pages/device_managment/ac/view/control_list/current_temp.dart +++ b/lib/pages/device_managment/ac/view/control_list/current_temp.dart @@ -1,11 +1,13 @@ import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/shared/celciuse_symbol.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/increament_decreament.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; class CurrentTemp extends StatefulWidget { const CurrentTemp({ @@ -50,7 +52,7 @@ class _CurrentTempState extends State { } _debounce = Timer(const Duration(milliseconds: 500), () { context.read().add( - AcControl( + AcControlEvent( deviceId: widget.deviceId, code: widget.code, value: (newValue * 10).toInt(), @@ -67,13 +69,7 @@ class _CurrentTempState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ColorsManager.greyColor.withOpacity(0.2), - border: Border.all(color: ColorsManager.boxDivider), - ), - padding: const EdgeInsets.all(16), + return DeviceControlsContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -83,10 +79,7 @@ class _CurrentTempState extends State { children: [ Text( 'Current Temperature', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Colors.grey), + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.grey), ), const SizedBox( height: 5, @@ -94,14 +87,8 @@ class _CurrentTempState extends State { Row( children: [ Text( - (widget.currentTemp > 99 - ? widget.currentTemp / 10 - : widget.currentTemp) - .toString(), - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Colors.grey), + (widget.currentTemp > 99 ? widget.currentTemp / 10 : widget.currentTemp).toString(), + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.grey), ), const CelsiusSymbol( color: Colors.grey, @@ -116,16 +103,20 @@ class _CurrentTempState extends State { description: '°C', descriptionColor: ColorsManager.dialogBlueTitle, onIncrement: () { - setState(() { - _adjustedValue++; - }); - _onValueChanged(_adjustedValue); + if (_adjustedValue < 30) { + setState(() { + _adjustedValue = _adjustedValue + 0.5; + }); + _onValueChanged(_adjustedValue); + } }, onDecrement: () { - setState(() { - _adjustedValue--; - }); - _onValueChanged(_adjustedValue); + if (_adjustedValue > 20) { + setState(() { + _adjustedValue = _adjustedValue - 0.5; + }); + _onValueChanged(_adjustedValue); + } }), ], ), diff --git a/lib/pages/device_managment/ac/view/control_list/fan_speed.dart b/lib/pages/device_managment/ac/view/control_list/fan_speed.dart index d8d61d6b..952e112b 100644 --- a/lib/pages/device_managment/ac/view/control_list/fan_speed.dart +++ b/lib/pages/device_managment/ac/view/control_list/fan_speed.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; +import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class FanSpeedControl extends StatelessWidget { const FanSpeedControl({ @@ -21,33 +22,24 @@ class FanSpeedControl extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ColorsManager.greyColor.withOpacity(0.2), - border: Border.all(color: ColorsManager.boxDivider), - ), - padding: const EdgeInsets.all(8), + return DeviceControlsContainer( child: Column( children: [ Wrap( runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, - value == FanSpeeds.auto), - _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, - value == FanSpeeds.low), + _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, value == FanSpeeds.auto), + _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, value == FanSpeeds.low), ], ), + const SizedBox(height: 8), Wrap( runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, - value == FanSpeeds.middle), - _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, - value == FanSpeeds.high), + _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, value == FanSpeeds.middle), + _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, value == FanSpeeds.high), ], ) ], @@ -55,12 +47,11 @@ class FanSpeedControl extends StatelessWidget { ); } - Widget _buildIconContainer(BuildContext context, FanSpeeds speed, - String assetPath, bool isSelected) { + Widget _buildIconContainer(BuildContext context, FanSpeeds speed, String assetPath, bool isSelected) { return GestureDetector( onTap: () { context.read().add( - AcControl( + AcControlEvent( deviceId: deviceId, code: code, value: speed.name, diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart index edb02cb9..8f6d085d 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart @@ -1,5 +1,5 @@ -import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -14,6 +14,10 @@ class DeviceManagementBloc int _offlineCount = 0; int _lowBatteryCount = 0; List _selectedDevices = []; + List _filteredDevices = []; + String currentProductName = ''; + String? currentCommunity; + String? currentUnitName; DeviceManagementBloc() : super(DeviceManagementInitial()) { on(_onFetchDevices); @@ -21,6 +25,9 @@ class DeviceManagementBloc on(_onSelectedFilterChanged); on(_onSearchDevices); on(_onSelectDevice); + on(_onResetFilters); + on(_onResetSelectedDevices); + on(_onUpdateSelection); } Future _onFetchDevices( @@ -28,14 +35,18 @@ class DeviceManagementBloc emit(DeviceManagementLoading()); try { final devices = await DevicesManagementApi().fetchDevices(); + _selectedDevices.clear(); _devices = devices; + _filteredDevices = devices; _calculateDeviceCounts(); emit(DeviceManagementLoaded( devices: devices, - selectedIndex: _selectedIndex, + selectedIndex: 0, onlineCount: _onlineCount, offlineCount: _offlineCount, lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, )); } catch (e) { emit(DeviceManagementInitial()); @@ -43,9 +54,9 @@ class DeviceManagementBloc } void _onFilterDevices( - FilterDevices event, Emitter emit) { + FilterDevices event, Emitter emit) async { if (_devices.isNotEmpty) { - final filteredDevices = _devices.where((device) { + _filteredDevices = List.from(_devices.where((device) { switch (event.filter) { case 'Online': return device.online == true; @@ -56,13 +67,64 @@ class DeviceManagementBloc default: return true; } - }).toList(); + }).toList()); + emit(DeviceManagementFiltered( - filteredDevices: filteredDevices, + filteredDevices: _filteredDevices, selectedIndex: _selectedIndex, onlineCount: _onlineCount, offlineCount: _offlineCount, lowBatteryCount: _lowBatteryCount, + selectedDevice: _selectedDevices.isNotEmpty ? _selectedDevices : null, + isControlButtonEnabled: _selectedDevices.isNotEmpty, + )); + + if (currentProductName.isNotEmpty) { + add(SearchDevices(productName: currentProductName)); + } + } + } + + Future _onResetFilters( + ResetFilters event, Emitter emit) async { + currentProductName = ''; + _selectedDevices.clear(); + _filteredDevices = List.from(_devices); + _selectedIndex = 0; + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: 0, + onlineCount: _onlineCount, + offlineCount: _offlineCount, + lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, + )); + } + + void _onResetSelectedDevices( + ResetSelectedDevices event, Emitter emit) { + _selectedDevices.clear(); + + if (state is DeviceManagementLoaded) { + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: _selectedIndex, + onlineCount: _onlineCount, + offlineCount: _offlineCount, + lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, + )); + } else if (state is DeviceManagementFiltered) { + emit(DeviceManagementFiltered( + filteredDevices: (state as DeviceManagementFiltered).filteredDevices, + selectedIndex: _selectedIndex, + onlineCount: _onlineCount, + offlineCount: _offlineCount, + lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, )); } } @@ -75,13 +137,18 @@ class DeviceManagementBloc void _onSelectDevice( SelectDevice event, Emitter emit) { - if (_selectedDevices.contains(event.selectedDevice)) { - _selectedDevices.remove(event.selectedDevice); + final selectedUuid = event.selectedDevice.uuid; + + if (_selectedDevices.any((device) => device.uuid == selectedUuid)) { + _selectedDevices.removeWhere((device) => device.uuid == selectedUuid); } else { _selectedDevices.add(event.selectedDevice); } - bool isControlButtonEnabled = _selectedDevices.length == 1; + List clonedSelectedDevices = List.from(_selectedDevices); + + bool isControlButtonEnabled = + _checkIfControlButtonEnabled(clonedSelectedDevices); if (state is DeviceManagementLoaded) { emit(DeviceManagementLoaded( @@ -90,7 +157,9 @@ class DeviceManagementBloc onlineCount: _onlineCount, offlineCount: _offlineCount, lowBatteryCount: _lowBatteryCount, - selectedDevice: isControlButtonEnabled ? _selectedDevices.first : null, + selectedDevice: + clonedSelectedDevices.isNotEmpty ? clonedSelectedDevices : null, + isControlButtonEnabled: isControlButtonEnabled, )); } else if (state is DeviceManagementFiltered) { emit(DeviceManagementFiltered( @@ -99,11 +168,66 @@ class DeviceManagementBloc onlineCount: _onlineCount, offlineCount: _offlineCount, lowBatteryCount: _lowBatteryCount, - selectedDevice: isControlButtonEnabled ? _selectedDevices.first : null, + selectedDevice: + clonedSelectedDevices.isNotEmpty ? clonedSelectedDevices : null, + isControlButtonEnabled: isControlButtonEnabled, )); } } + void _onUpdateSelection( + UpdateSelection event, Emitter emit) { + List selectedDevices = []; + List devicesToSelectFrom = []; + + if (state is DeviceManagementLoaded) { + devicesToSelectFrom = (state as DeviceManagementLoaded).devices; + } else if (state is DeviceManagementFiltered) { + devicesToSelectFrom = (state as DeviceManagementFiltered).filteredDevices; + } + + for (int i = 0; i < event.selectedRows.length; i++) { + if (event.selectedRows[i]) { + selectedDevices.add(devicesToSelectFrom[i]); + } + } + + if (state is DeviceManagementLoaded) { + final loadedState = state as DeviceManagementLoaded; + emit(DeviceManagementLoaded( + devices: loadedState.devices, + selectedIndex: loadedState.selectedIndex, + onlineCount: loadedState.onlineCount, + offlineCount: loadedState.offlineCount, + lowBatteryCount: loadedState.lowBatteryCount, + selectedDevice: selectedDevices, + isControlButtonEnabled: _checkIfControlButtonEnabled(selectedDevices), + )); + } else if (state is DeviceManagementFiltered) { + final filteredState = state as DeviceManagementFiltered; + emit(DeviceManagementFiltered( + filteredDevices: filteredState.filteredDevices, + selectedIndex: filteredState.selectedIndex, + onlineCount: filteredState.onlineCount, + offlineCount: filteredState.offlineCount, + lowBatteryCount: filteredState.lowBatteryCount, + selectedDevice: selectedDevices, + isControlButtonEnabled: _checkIfControlButtonEnabled(selectedDevices), + )); + } + } + + bool _checkIfControlButtonEnabled(List selectedDevices) { + if (selectedDevices.length > 1) { + final productTypes = + selectedDevices.map((device) => device.productType).toSet(); + return productTypes.length == 1; + } else if (selectedDevices.length == 1) { + return true; + } + return false; + } + void _calculateDeviceCounts() { _onlineCount = _devices.where((device) => device.online == true).length; _offlineCount = _devices.where((device) => device.online == false).length; @@ -128,27 +252,61 @@ class DeviceManagementBloc void _onSearchDevices( SearchDevices event, Emitter emit) { - if (_devices.isNotEmpty) { - final filteredDevices = _devices.where((device) { + if ((event.community == null || event.community!.isEmpty) && + (event.unitName == null || event.unitName!.isEmpty) && + (event.productName == null || event.productName!.isEmpty)) { + currentProductName = ''; + if (state is DeviceManagementFiltered) { + add(FilterDevices(_getFilterFromIndex(_selectedIndex))); + } else { + return; + } + } + + if (event.productName == currentProductName && + event.community == currentCommunity && + event.unitName == currentUnitName && + event.searchField) { + return; + } + + currentProductName = event.productName ?? ''; + currentCommunity = event.community; + currentUnitName = event.unitName; + + List devicesToSearch = _filteredDevices; + + if (devicesToSearch.isNotEmpty) { + final filteredDevices = devicesToSearch.where((device) { final matchesCommunity = event.community == null || event.community!.isEmpty || - (device.room?.name + (device.community?.name ?.toLowerCase() .contains(event.community!.toLowerCase()) ?? false); final matchesUnit = event.unitName == null || event.unitName!.isEmpty || - (device.unit?.name - ?.toLowerCase() - .contains(event.unitName!.toLowerCase()) ?? - false); + (device.spaces != null && + device.spaces!.isNotEmpty && + device.spaces![0].spaceName + !.toLowerCase() + .contains(event.unitName!.toLowerCase())); final matchesProductName = event.productName == null || event.productName!.isEmpty || (device.name ?.toLowerCase() .contains(event.productName!.toLowerCase()) ?? false); - return matchesCommunity && matchesUnit && matchesProductName; + final matchesDeviceName = event.productName == null || + event.productName!.isEmpty || + (device.categoryName + ?.toLowerCase() + .contains(event.productName!.toLowerCase()) ?? + false); + + return matchesCommunity && + matchesUnit && + (matchesProductName || matchesDeviceName); }).toList(); emit(DeviceManagementFiltered( @@ -157,6 +315,8 @@ class DeviceManagementBloc onlineCount: _onlineCount, offlineCount: _offlineCount, lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, )); } } diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_managment_event.dart index 444f3406..c7509080 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_managment_event.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_managment_event.dart @@ -31,11 +31,13 @@ class SearchDevices extends DeviceManagementEvent { final String? community; final String? unitName; final String? productName; + final bool searchField; const SearchDevices({ this.community, this.unitName, this.productName, + this.searchField = false, }); @override @@ -50,3 +52,13 @@ class SelectDevice extends DeviceManagementEvent { @override List get props => [selectedDevice]; } + +class ResetFilters extends DeviceManagementEvent {} + +class ResetSelectedDevices extends DeviceManagementEvent {} + +class UpdateSelection extends DeviceManagementEvent { + final List selectedRows; + + const UpdateSelection(this.selectedRows); +} diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_state.dart b/lib/pages/device_managment/all_devices/bloc/device_managment_state.dart index 9a6e2f41..f7513890 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_managment_state.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_managment_state.dart @@ -17,7 +17,8 @@ class DeviceManagementLoaded extends DeviceManagementState { final int onlineCount; final int offlineCount; final int lowBatteryCount; - final AllDevicesModel? selectedDevice; + final List? selectedDevice; + final bool isControlButtonEnabled; const DeviceManagementLoaded({ required this.devices, @@ -26,6 +27,7 @@ class DeviceManagementLoaded extends DeviceManagementState { required this.offlineCount, required this.lowBatteryCount, this.selectedDevice, + required this.isControlButtonEnabled, }); @override @@ -35,7 +37,8 @@ class DeviceManagementLoaded extends DeviceManagementState { onlineCount, offlineCount, lowBatteryCount, - selectedDevice + selectedDevice, + isControlButtonEnabled ]; } @@ -45,7 +48,8 @@ class DeviceManagementFiltered extends DeviceManagementState { final int onlineCount; final int offlineCount; final int lowBatteryCount; - final AllDevicesModel? selectedDevice; + final List? selectedDevice; + final bool isControlButtonEnabled; const DeviceManagementFiltered({ required this.filteredDevices, @@ -54,6 +58,7 @@ class DeviceManagementFiltered extends DeviceManagementState { required this.offlineCount, required this.lowBatteryCount, this.selectedDevice, + required this.isControlButtonEnabled, }); @override @@ -63,7 +68,8 @@ class DeviceManagementFiltered extends DeviceManagementState { onlineCount, offlineCount, lowBatteryCount, - selectedDevice + selectedDevice, + isControlButtonEnabled ]; } diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index fbe1f198..e38ac582 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -1,34 +1,199 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/ac/view/ac_device_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/ac_device_control.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart'; -import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_status_view.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart'; +import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/view/living_room_device_control.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/smart_power_device_control.dart'; +import 'package:syncrow_web/pages/device_managment/sos/view/sos_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/sos/view/sos_device_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/view/living_room_device_control.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heater_batch_control.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heater_device_control.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart'; + +import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; mixin RouteControlsBasedCode { Widget routeControlsWidgets({required AllDevicesModel device}) { switch (device.productType) { + case '1G': + return WallLightDeviceControl( + deviceId: device.uuid!, + ); + case '2G': + return TwoGangDeviceControlView( + deviceId: device.uuid!, + ); case '3G': - return LivingRoomDeviceControl( + return LivingRoomDeviceControlsView( + deviceId: device.uuid!, + ); + case '1GT': + return OneGangGlassSwitchControlView( + deviceId: device.uuid!, + ); + case '2GT': + return TwoGangGlassSwitchControlView( + deviceId: device.uuid!, + ); + case '3GT': + return ThreeGangGlassSwitchControlView( deviceId: device.uuid!, ); case 'GW': - return GateWayControls( + return GateWayControlsView( gatewayId: device.uuid!, ); case 'DL': - return DoorLockView(device: device); + return DoorLockControlsView(device: device); case 'WPS': - return WallSensorControls(device: device); + return WallSensorControlsView(device: device); case 'CPS': - return CeilingSensorControls( + return CeilingSensorControlsView( device: device, ); + case 'CUR': + return CurtainStatusControlsView( + deviceId: device.uuid!, + ); case 'AC': - return AcDeviceControl(device: device); + return AcDeviceControlsView(device: device); + case 'WH': + return WaterHeaterDeviceControlView( + device: device, + ); + case 'DS': + return MainDoorSensorControlView(device: device); + case 'GD': + return GarageDoorControlView( + deviceId: device.uuid!, + ); + case 'WL': + return WaterLeakView( + deviceId: device.uuid!, + ); + case 'PC': + return SmartPowerDeviceControl( + deviceId: device.uuid!, + ); + case 'SOS': + return SosDeviceControlsView(device: device); + default: + return const SizedBox(); + } + } + + /* + 3G: + 1G: + 2G: + GW: + DL: + WPS: + CPS: + AC: + CUR: + WH: + DS: + */ + + Widget routeBatchControlsWidgets({required List devices}) { + switch (devices.first.productType) { + case '1G': + return WallLightBatchControlView( + deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(), + ); + case '2G': + return TwoGangBatchControlView( + deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(), + ); + case '3G': + return LivingRoomBatchControlsView( + deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(), + ); + case '1GT': + return OneGangGlassSwitchBatchControlView( + deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(), + ); + case '2GT': + return TwoGangGlassSwitchBatchControlView( + deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(), + ); + case '3GT': + return ThreeGangGlassSwitchBatchControlView( + deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(), + ); + case 'GW': + return GatewayBatchControlView( + gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(), + ); + case 'DL': + return DoorLockBatchControlView( + devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList()); + case 'WPS': + return WallSensorBatchControlView( + devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList()); + case 'CPS': + return CeilingSensorBatchControlView( + devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(), + ); + case 'CUR': + return CurtainBatchStatusView( + devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(), + ); + case 'AC': + return AcDeviceBatchControlView( + devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList()); + case 'WH': + return WaterHEaterBatchControlView( + deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(), + ); + case 'DS': + return MainDoorSensorBatchView( + devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(), + ); + case 'GD': + return GarageDoorBatchControlView( + deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(), + ); + case 'WL': + return WaterLeakBatchControlView( + deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(), + ); + case 'PC': + return PowerClampBatchControlView( + deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(), + ); + case 'SOS': + return SOSBatchControlView( + deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(), + ); default: return const SizedBox(); } diff --git a/lib/pages/device_managment/all_devices/models/device_community.model.dart b/lib/pages/device_managment/all_devices/models/device_community.model.dart new file mode 100644 index 00000000..6f91cab6 --- /dev/null +++ b/lib/pages/device_managment/all_devices/models/device_community.model.dart @@ -0,0 +1,18 @@ +class DeviceCommunityModel { + String? uuid; + String? name; + + DeviceCommunityModel({this.uuid, this.name}); + + DeviceCommunityModel.fromJson(Map json) { + uuid = json['uuid']?.toString(); + name = json['name']?.toString(); + } + + Map toJson() { + final data = {}; + data['uuid'] = uuid; + data['name'] = name; + return data; + } +} diff --git a/lib/pages/device_managment/all_devices/models/device_reports.dart b/lib/pages/device_managment/all_devices/models/device_reports.dart index 05604b25..82c49386 100644 --- a/lib/pages/device_managment/all_devices/models/device_reports.dart +++ b/lib/pages/device_managment/all_devices/models/device_reports.dart @@ -13,11 +13,15 @@ class DeviceReport { DeviceReport.fromJson(Map json) : deviceUuid = json['deviceUuid'] as String?, - startTime = json['startTime'] as int?, - endTime = json['endTime'] as int?, - data = (json['data'] as List?) - ?.map((e) => DeviceEvent.fromJson(e as Map)) - .toList(); + startTime = int.tryParse(json['startTime'].toString()) ?? + json['startTime'] as int?, + endTime = + int.tryParse(json['endTime'].toString()) ?? json['endTime'] as int?, + data = json['data'] != null + ? (json['data'] as List?) + ?.map((e) => DeviceEvent.fromJson(e as Map)) + .toList() + : []; Map toJson() => { 'deviceUuid': deviceUuid, diff --git a/lib/pages/device_managment/all_devices/models/device_space_model.dart b/lib/pages/device_managment/all_devices/models/device_space_model.dart new file mode 100644 index 00000000..29f80142 --- /dev/null +++ b/lib/pages/device_managment/all_devices/models/device_space_model.dart @@ -0,0 +1,18 @@ +class DeviceSpaceModel { + String? uuid; + String? spaceName; + + DeviceSpaceModel({this.uuid, this.spaceName}); + + DeviceSpaceModel.fromJson(Map json) { + uuid = json['uuid']?.toString(); + spaceName = json['spaceName']?.toString(); + } + + Map toJson() { + final data = {}; + data['uuid'] = uuid; + data['spaceName'] = spaceName; + return data; + } +} diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index 48e99a1a..df80c3e7 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -1,5 +1,8 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_community.model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_space_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class AllDevicesModel { /* @@ -39,6 +42,7 @@ class AllDevicesModel { DevicesModelRoom? room; DevicesModelUnit? unit; + DeviceCommunityModel? community; String? productUuid; String? productType; String? permissionType; @@ -62,10 +66,13 @@ class AllDevicesModel { int? updateTime; String? uuid; int? batteryLevel; + String? productName; + List? spaces; AllDevicesModel({ this.room, this.unit, + this.community, this.productUuid, this.productType, this.permissionType, @@ -89,6 +96,8 @@ class AllDevicesModel { this.updateTime, this.uuid, this.batteryLevel, + this.productName, + this.spaces, }); AllDevicesModel.fromJson(Map json) { room = (json['room'] != null && (json['room'] is Map)) @@ -97,6 +106,9 @@ class AllDevicesModel { unit = (json['unit'] != null && (json['unit'] is Map)) ? DevicesModelUnit.fromJson(json['unit']) : null; + community = (json['community'] != null && (json['community'] is Map)) + ? DeviceCommunityModel.fromJson(json['community']) + : null; productUuid = json['productUuid']?.toString(); productType = json['productType']?.toString(); permissionType = json['permissionType']?.toString(); @@ -105,7 +117,7 @@ class AllDevicesModel { categoryName = json['categoryName']?.toString(); createTime = int.tryParse(json['createTime']?.toString() ?? ''); gatewayId = json['gatewayId']?.toString(); - icon = json['icon']?.toString(); + icon = json['icon'] ?? _getDefaultIcon(productType); ip = json['ip']?.toString(); lat = json['lat']?.toString(); localKey = json['localKey']?.toString(); @@ -119,8 +131,43 @@ class AllDevicesModel { timeZone = json['timeZone']?.toString(); updateTime = int.tryParse(json['updateTime']?.toString() ?? ''); uuid = json['uuid']?.toString(); - batteryLevel = int.tryParse(json['batteryLevel']?.toString() ?? ''); + batteryLevel = int.tryParse(json['battery']?.toString() ?? ''); + productName = json['productName']?.toString(); + if (json['spaces'] != null && json['spaces'] is List) { + spaces = (json['spaces'] as List) + .map((space) => DeviceSpaceModel.fromJson(space)) + .toList(); + } } + + String _getDefaultIcon(String? productType) { + switch (productType) { + case 'LightBulb': + return Assets.lightBulb; + case 'CeilingSensor': + case 'WallSensor': + return Assets.sensors; + case 'AC': + return Assets.ac; + case 'DoorLock': + return Assets.doorLock; + case 'Curtain': + return Assets.curtain; + case '3G': + case '2G': + case '1G': + return Assets.gangSwitch; + case 'Gateway': + return Assets.gateway; + case 'WH': + return Assets.blackLogo; + case 'DS': + return Assets.sensors; + default: + return Assets.logo; + } + } + Map toJson() { final data = {}; if (room != null) { @@ -129,6 +176,9 @@ class AllDevicesModel { if (unit != null) { data['unit'] = unit!.toJson(); } + if (community != null) { + data['community'] = community!.toJson(); + } data['productUuid'] = productUuid; data['productType'] = productType; data['permissionType'] = permissionType; @@ -151,7 +201,74 @@ class AllDevicesModel { data['timeZone'] = timeZone; data['updateTime'] = updateTime; data['uuid'] = uuid; - data['batteryLevel'] = batteryLevel; + data['battery'] = batteryLevel; + data['productName'] = productName; + if (spaces != null) { + data['spaces'] = spaces!.map((space) => space.toJson()).toList(); + } return data; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is AllDevicesModel && + other.room == room && + other.unit == unit && + other.productUuid == productUuid && + other.productType == productType && + other.permissionType == permissionType && + other.activeTime == activeTime && + other.category == category && + other.categoryName == categoryName && + other.createTime == createTime && + other.gatewayId == gatewayId && + other.icon == icon && + other.ip == ip && + other.lat == lat && + other.localKey == localKey && + other.lon == lon && + other.model == model && + other.name == name && + other.nodeId == nodeId && + other.online == online && + other.ownerId == ownerId && + other.sub == sub && + other.timeZone == timeZone && + other.updateTime == updateTime && + other.uuid == uuid && + other.productName == productName && + other.batteryLevel == batteryLevel; + } + + @override + int get hashCode { + return room.hashCode ^ + unit.hashCode ^ + productUuid.hashCode ^ + productType.hashCode ^ + permissionType.hashCode ^ + activeTime.hashCode ^ + category.hashCode ^ + categoryName.hashCode ^ + createTime.hashCode ^ + gatewayId.hashCode ^ + icon.hashCode ^ + ip.hashCode ^ + lat.hashCode ^ + localKey.hashCode ^ + lon.hashCode ^ + model.hashCode ^ + name.hashCode ^ + nodeId.hashCode ^ + online.hashCode ^ + ownerId.hashCode ^ + sub.hashCode ^ + timeZone.hashCode ^ + updateTime.hashCode ^ + uuid.hashCode ^ + productName.hashCode ^ + batteryLevel.hashCode; + } } diff --git a/lib/pages/device_managment/all_devices/models/factory_reset_model.dart b/lib/pages/device_managment/all_devices/models/factory_reset_model.dart new file mode 100644 index 00000000..aec14d16 --- /dev/null +++ b/lib/pages/device_managment/all_devices/models/factory_reset_model.dart @@ -0,0 +1,55 @@ +import 'package:flutter/foundation.dart'; + +class FactoryResetModel { + final List devicesUuid; + + FactoryResetModel({ + required this.devicesUuid, + }); + + factory FactoryResetModel.fromJson(Map json) { + return FactoryResetModel( + devicesUuid: List.from(json['devicesUuid']), + ); + } + + Map toJson() { + return { + 'devicesUuid': devicesUuid, + }; + } + + FactoryResetModel copyWith({ + List? devicesUuid, + }) { + return FactoryResetModel( + devicesUuid: devicesUuid ?? this.devicesUuid, + ); + } + + Map toMap() { + return { + 'devicesUuid': devicesUuid, + }; + } + + factory FactoryResetModel.fromMap(Map map) { + return FactoryResetModel( + devicesUuid: List.from(map['devicesUuid']), + ); + } + + @override + String toString() => 'FactoryReset(devicesUuid: $devicesUuid)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is FactoryResetModel && + listEquals(other.devicesUuid, devicesUuid); + } + + @override + int get hashCode => devicesUuid.hashCode; +} diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index c56a661f..8ed8c35e 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -13,24 +14,25 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { return BlocProvider( create: (context) => DeviceManagementBloc()..add(FetchDevices()), child: WebScaffold( - appBarTitle: Text( - 'Device Management', - style: Theme.of(context).textTheme.headlineLarge, + appBarTitle: FittedBox( + child: Text( + 'Device Management', + style: Theme.of(context).textTheme.headlineLarge, + ), ), - enableMenuSideba: isLargeScreenSize(context), + rightBody: const NavigateHomeGridView(), scaffoldBody: BlocBuilder( builder: (context, state) { if (state is DeviceManagementLoading) { return const Center(child: CircularProgressIndicator()); - } else if (state is DeviceManagementLoaded || - state is DeviceManagementFiltered) { + } else if (state is DeviceManagementLoaded || state is DeviceManagementFiltered) { final devices = state is DeviceManagementLoaded ? state.devices : (state as DeviceManagementFiltered).filteredDevices; return DeviceManagementBody(devices: devices); } else { - return const Center(child: Text('No Devices Found')); + return const Center(child: Text('Error fetching Devices')); } }, ), @@ -38,3 +40,6 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); } } + + + diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index 3fb8b300..12c66403 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/core/extension/build_context_x.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/custom_table.dart'; import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_control_dialog.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_control_dialog.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -27,6 +27,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { int offlineCount = 0; int lowBatteryCount = 0; bool isControlButtonEnabled = false; + List selectedDevices = []; if (state is DeviceManagementLoaded) { devicesToShow = state.devices; @@ -34,87 +35,108 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { onlineCount = state.onlineCount; offlineCount = state.offlineCount; lowBatteryCount = state.lowBatteryCount; - isControlButtonEnabled = state.selectedDevice != null; + isControlButtonEnabled = state.isControlButtonEnabled; + selectedDevices = state.selectedDevice ?? []; } else if (state is DeviceManagementFiltered) { devicesToShow = state.filteredDevices; selectedIndex = state.selectedIndex; onlineCount = state.onlineCount; offlineCount = state.offlineCount; lowBatteryCount = state.lowBatteryCount; - isControlButtonEnabled = state.selectedDevice != null; + isControlButtonEnabled = state.isControlButtonEnabled; + selectedDevices = state.selectedDevice ?? []; + } else if (state is DeviceManagementInitial) { + devicesToShow = []; + selectedIndex = 0; + isControlButtonEnabled = false; } - final tabs = [ - 'All (${devices.length})', + 'All', 'Online ($onlineCount)', 'Offline ($offlineCount)', 'Low Battery ($lowBatteryCount)', ]; - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Container( - padding: isLargeScreenSize(context) - ? const EdgeInsets.all(30) - : const EdgeInsets.all(15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FilterWidget( - size: MediaQuery.of(context).size, - tabs: tabs, - selectedIndex: selectedIndex, - onTabChanged: (index) { - context - .read() - .add(SelectedFilterChanged(index)); - }, - ), - const SizedBox(height: 20), - const DeviceSearchFilters(), - const SizedBox(height: 12), - Container( - height: 43, - width: isSmallScreenSize(context) ? double.infinity : 100, - decoration: containerDecoration, - child: Center( - child: DefaultButton( - onPressed: isControlButtonEnabled - ? () { - final selectedDevice = context - .read() - .selectedDevices - .first; + final buttonLabel = + (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + + return Column( + children: [ + Container( + padding: isLargeScreenSize(context) + ? const EdgeInsets.all(30) + : const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FilterWidget( + size: MediaQuery.of(context).size, + tabs: tabs, + selectedIndex: selectedIndex, + onTabChanged: (index) { + context + .read() + .add(SelectedFilterChanged(index)); + }, + ), + const SizedBox(height: 20), + const DeviceSearchFilters(), + const SizedBox(height: 12), + Container( + height: 45, + width: 125, + decoration: containerDecoration, + child: Center( + child: DefaultButton( + onPressed: isControlButtonEnabled + ? () { + if (selectedDevices.length == 1) { showDialog( context: context, builder: (context) => DeviceControlDialog( - device: selectedDevice), + device: selectedDevices.first, + ), ); + } else if (selectedDevices.length > 1) { + final productTypes = selectedDevices + .map((device) => device.productType) + .toSet(); + if (productTypes.length == 1) { + showDialog( + context: context, + builder: (context) => + DeviceBatchControlDialog( + devices: selectedDevices, + ), + ); + } } - : null, - borderRadius: 9, - child: Text( - 'Control', - style: TextStyle( - color: isControlButtonEnabled - ? Colors.white - : Colors.grey, - ), + } + : null, + borderRadius: 9, + child: Text( + buttonLabel, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: isControlButtonEnabled + ? Colors.white + : Colors.grey, ), ), ), ), - ], - ), + ), + ], ), ), - SliverFillRemaining( + Expanded( child: Padding( padding: isLargeScreenSize(context) ? const EdgeInsets.all(30) : const EdgeInsets.all(15), child: DynamicTable( + withSelectAll: true, cellDecoration: containerDecoration, onRowSelected: (index, isSelected, row) { final selectedDevice = devicesToShow[index]; @@ -123,28 +145,42 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .add(SelectDevice(selectedDevice)); }, withCheckBox: true, - size: context.screenSize, + size: MediaQuery.of(context).size, + uuidIndex: 2, headers: const [ 'Device Name', 'Product Name', 'Device ID', - 'Unit Name', - 'Room', + 'Space Name', + 'location', 'Battery Level', 'Installation Date and Time', 'Status', 'Last Offline Date and Time', ], data: devicesToShow.map((device) { + final combinedSpaceNames = device.spaces != null + ? device.spaces! + .map((space) => space.spaceName) + .join(' > ') + + (device.community != null + ? ' > ${device.community!.name}' + : '') + : (device.community != null + ? device.community!.name + : ''); + return [ - device.categoryName ?? '', device.name ?? '', + device.productName ?? '', device.uuid ?? '', - device.unit?.name ?? '', - device.room?.name ?? '', + (device.spaces != null && device.spaces!.isNotEmpty) + ? device.spaces![0].spaceName + : '', + combinedSpaceNames, device.batteryLevel != null ? '${device.batteryLevel}%' - : '', + : '-', formatDateTime(DateTime.fromMillisecondsSinceEpoch( (device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', @@ -152,10 +188,20 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { (device.updateTime ?? 0) * 1000)), ]; }).toList(), + onSelectionChanged: (selectedRows) { + context + .read() + .add(UpdateSelection(selectedRows)); + }, + initialSelectedIds: context + .read() + .selectedDevices + .map((device) => device.uuid!) + .toList(), isEmpty: devicesToShow.isEmpty, ), ), - ), + ) ], ); }, diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index 7af6293e..71974156 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; +import 'package:syncrow_web/utils/style.dart'; class DeviceSearchFilters extends StatefulWidget { const DeviceSearchFilters({super.key}); @@ -28,12 +29,12 @@ class _DeviceSearchFiltersState extends State @override Widget build(BuildContext context) { - return isLargeScreenSize(context) + return isExtraLargeScreenSize(context) ? Row( children: [ _buildSearchField("Community", communityController, 200), const SizedBox(width: 20), - _buildSearchField("Unit Name", unitNameController, 200), + _buildSearchField("Space Name", unitNameController, 200), const SizedBox(width: 20), _buildSearchField( "Device Name / Product Name", productNameController, 300), @@ -45,10 +46,17 @@ class _DeviceSearchFiltersState extends State spacing: 20, runSpacing: 10, children: [ - _buildSearchField("Community", communityController, 200), - _buildSearchField("Unit Name", unitNameController, 200), _buildSearchField( - "Device Name / Product Name", productNameController, 300), + "Community", + communityController, + 200, + ), + _buildSearchField("Space Name", unitNameController, 200), + _buildSearchField( + "Device Name / Product Name", + productNameController, + 300, + ), _buildSearchResetButtons(), ], ); @@ -56,11 +64,20 @@ class _DeviceSearchFiltersState extends State Widget _buildSearchField( String title, TextEditingController controller, double width) { - return StatefulTextField( - title: title, - width: width, - elevation: 2, - controller: controller, + return Container( + child: StatefulTextField( + title: title, + width: width, + elevation: 2, + controller: controller, + onSubmitted: () { + context.read().add(SearchDevices( + productName: productNameController.text, + unitName: unitNameController.text, + community: communityController.text, + searchField: true)); + }, + ), ); } @@ -68,16 +85,18 @@ class _DeviceSearchFiltersState extends State return SearchResetButtons( onSearch: () { context.read().add(SearchDevices( - community: communityController.text, - unitName: unitNameController.text, - productName: productNameController.text, - )); + community: communityController.text, + unitName: unitNameController.text, + productName: productNameController.text, + searchField: true)); }, onReset: () { communityController.clear(); unitNameController.clear(); productNameController.clear(); - context.read().add(FetchDevices()); + context.read() + ..add(ResetFilters()) + ..add(FetchDevices()); }, ); } diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart b/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart deleted file mode 100644 index fe928c94..00000000 --- a/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'dart:async'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/event.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/state.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/help_description.dart'; -import 'package:syncrow_web/services/devices_mang_api.dart'; - -class CeilingSensorBloc extends Bloc { - final String deviceId; - late CeilingSensorModel deviceStatus; - Timer? _timer; - - CeilingSensorBloc({required this.deviceId}) : super(CeilingInitialState()) { - on(_fetchCeilingSensorStatus); - on(_changeValue); - on(_getDeviceReports); - on(_showDescription); - on(_backToGridView); - } - - void _fetchCeilingSensorStatus( - CeilingInitialEvent event, Emitter emit) async { - emit(CeilingLoadingInitialState()); - try { - var response = await DevicesManagementApi().getDeviceStatus(deviceId); - deviceStatus = CeilingSensorModel.fromJson(response.status); - emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - // _listenToChanges(); - } catch (e) { - emit(CeilingFailedState(error: e.toString())); - return; - } - } - - // _listenToChanges() { - // try { - // DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - // Stream stream = ref.onValue; - - // stream.listen((DatabaseEvent event) { - // Map usersMap = event.snapshot.value as Map; - // List statusList = []; - - // usersMap['status'].forEach((element) { - // statusList.add(StatusModel(code: element['code'], value: element['value'])); - // }); - - // deviceStatus = WallSensorModel.fromJson(statusList); - // add(WallSensorUpdatedEvent()); - // }); - // } catch (_) {} - // } - - void _changeValue( - CeilingChangeValueEvent event, Emitter emit) async { - emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } - emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, code: event.code, value: event.value); - } - - _runDeBouncer({ - required String deviceId, - required String code, - required dynamic value, - }) { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - - if (!response) { - add(CeilingInitialEvent()); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(CeilingInitialEvent()); - } - }); - } - - FutureOr _getDeviceReports(GetCeilingDeviceReportsEvent event, - Emitter emit) async { - if (event.code.isEmpty) { - emit(ShowCeilingDescriptionState(description: reportString)); - return; - } else { - emit(CeilingReportsLoadingState()); - - try { - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(CeilingReportsState(deviceReport: value)); - }); - } catch (e) { - emit(CeilingReportsFailedState(error: e.toString())); - return; - } - } - } - - void _showDescription( - ShowCeilingDescriptionEvent event, Emitter emit) { - emit(ShowCeilingDescriptionState(description: event.description)); - } - - void _backToGridView( - BackToCeilingGridViewEvent event, Emitter emit) { - emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - } -} diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart new file mode 100644 index 00000000..4e13bfd6 --- /dev/null +++ b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/help_description.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class CeilingSensorBloc extends Bloc { + final String deviceId; + late CeilingSensorModel deviceStatus; + Timer? _timer; + + CeilingSensorBloc({required this.deviceId}) : super(CeilingInitialState()) { + on(_fetchCeilingSensorStatus); + on(_fetchCeilingSensorBatchControl); + on(_changeValue); + on(_onBatchControl); + on(_getDeviceReports); + on(_showDescription); + on(_backToGridView); + on(_onFactoryReset); + } + + void _fetchCeilingSensorStatus( + CeilingInitialEvent event, Emitter emit) async { + emit(CeilingLoadingInitialState()); + try { + var response = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = CeilingSensorModel.fromJson(response.status); + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + // _listenToChanges(); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + return; + } + } + + // _listenToChanges() { + // try { + // DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + // Stream stream = ref.onValue; + + // stream.listen((DatabaseEvent event) { + // Map usersMap = event.snapshot.value as Map; + // List statusList = []; + + // usersMap['status'].forEach((element) { + // statusList.add(StatusModel(code: element['code'], value: element['value'])); + // }); + + // deviceStatus = WallSensorModel.fromJson(statusList); + // add(WallSensorUpdatedEvent()); + // }); + // } catch (_) {} + // } + + void _changeValue(CeilingChangeValueEvent event, Emitter emit) async { + emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); + if (event.code == 'sensitivity') { + deviceStatus.sensitivity = event.value; + } else if (event.code == 'none_body_time') { + deviceStatus.noBodyTime = event.value; + } else if (event.code == 'moving_max_dis') { + deviceStatus.maxDistance = event.value; + } else if (event.code == 'scene') { + deviceStatus.spaceType = getSpaceType(event.value); + } + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + await _runDeBouncer( + deviceId: deviceId, + code: event.code, + value: event.value, + emit: emit, + isBatch: false, + ); + } + + Future _onBatchControl( + CeilingBatchControlEvent event, Emitter emit) async { + emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); + if (event.code == 'sensitivity') { + deviceStatus.sensitivity = event.value; + } else if (event.code == 'none_body_time') { + deviceStatus.noBodyTime = event.value; + } else if (event.code == 'moving_max_dis') { + deviceStatus.maxDistance = event.value; + } else if (event.code == 'scene') { + deviceStatus.spaceType = getSpaceType(event.value); + } + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + await _runDeBouncer( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + emit: emit, + isBatch: true, + ); + } + + _runDeBouncer({ + required dynamic deviceId, + required String code, + required dynamic value, + required Emitter emit, + required bool isBatch, + }) { + late String id; + + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(const Duration(seconds: 1), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi().deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + add(CeilingInitialEvent(id)); + } + if (response == true && code == 'scene') { + emit(CeilingLoadingInitialState()); + await Future.delayed(const Duration(seconds: 1)); + add(CeilingInitialEvent(id)); + } + } catch (_) { + await Future.delayed(const Duration(milliseconds: 500)); + add(CeilingInitialEvent(id)); + } + }); + } + + FutureOr _getDeviceReports( + GetCeilingDeviceReportsEvent event, Emitter emit) async { + if (event.code.isEmpty) { + emit(ShowCeilingDescriptionState(description: reportString)); + return; + } else { + emit(CeilingReportsLoadingState()); + // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; + // final to = DateTime.now().millisecondsSinceEpoch; + + try { + // await DevicesManagementApi.getDeviceReportsByDate(deviceId, event.code, from.toString(), to.toString()) + await DevicesManagementApi.getDeviceReports(deviceId, event.code).then((value) { + emit(CeilingReportsState(deviceReport: value)); + }); + } catch (e) { + emit(CeilingReportsFailedState(error: e.toString())); + return; + } + } + } + + void _showDescription(ShowCeilingDescriptionEvent event, Emitter emit) { + emit(ShowCeilingDescriptionState(description: event.description)); + } + + void _backToGridView(BackToCeilingGridViewEvent event, Emitter emit) { + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + } + + FutureOr _fetchCeilingSensorBatchControl( + CeilingFetchDeviceStatusEvent event, Emitter emit) async { + emit(CeilingLoadingInitialState()); + try { + var response = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = CeilingSensorModel.fromJson(response.status); + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + return; + } + } + + FutureOr _onFactoryReset( + CeilingFactoryResetEvent event, Emitter emit) async { + emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryResetModel, + event.devicesId, + ); + if (!response) { + emit(const CeilingFailedState(error: 'Failed')); + } else { + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + } + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } + } +} diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart new file mode 100644 index 00000000..582c9836 --- /dev/null +++ b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart @@ -0,0 +1,85 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +abstract class CeilingSensorEvent extends Equatable { + const CeilingSensorEvent(); + + @override + List get props => []; +} + +class CeilingInitialEvent extends CeilingSensorEvent { + final String deviceId; + const CeilingInitialEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class CeilingFetchDeviceStatusEvent extends CeilingSensorEvent { + final List devicesIds; + + const CeilingFetchDeviceStatusEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class CeilingBatchControlEvent extends CeilingSensorEvent { + final List deviceIds; + final String code; + final dynamic value; + + const CeilingBatchControlEvent({ + required this.deviceIds, + required this.code, + required this.value, + }); + + @override + List get props => [deviceIds, code, value]; +} + +class CeilingChangeValueEvent extends CeilingSensorEvent { + final dynamic value; + final String code; + const CeilingChangeValueEvent({required this.value, required this.code}); + + @override + List get props => [value, code]; +} + +class GetCeilingDeviceReportsEvent extends CeilingSensorEvent { + final String code; + final String deviceUuid; + + const GetCeilingDeviceReportsEvent( + {required this.code, required this.deviceUuid}); + + @override + List get props => [code, deviceUuid]; +} + +class ShowCeilingDescriptionEvent extends CeilingSensorEvent { + final String description; + + const ShowCeilingDescriptionEvent({required this.description}); + + @override + List get props => [description]; +} + +class BackToCeilingGridViewEvent extends CeilingSensorEvent {} + +class CeilingFactoryResetEvent extends CeilingSensorEvent { + final String devicesId; + final FactoryResetModel factoryResetModel; + + const CeilingFactoryResetEvent({ + required this.devicesId, + required this.factoryResetModel, + }); + + @override + List get props => [devicesId, factoryResetModel]; +} diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/state.dart b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart similarity index 100% rename from lib/pages/device_managment/ceiling_sensor/bloc/state.dart rename to lib/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/event.dart b/lib/pages/device_managment/ceiling_sensor/bloc/event.dart deleted file mode 100644 index 31c5ab56..00000000 --- a/lib/pages/device_managment/ceiling_sensor/bloc/event.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:equatable/equatable.dart'; - -abstract class CeilingSensorEvent extends Equatable { - const CeilingSensorEvent(); - - @override - List get props => []; -} - -class CeilingInitialEvent extends CeilingSensorEvent {} - -class CeilingChangeValueEvent extends CeilingSensorEvent { - final dynamic value; - final String code; - const CeilingChangeValueEvent({required this.value, required this.code}); - - @override - List get props => [value, code]; -} - -class GetCeilingDeviceReportsEvent extends CeilingSensorEvent { - final String code; - final String deviceUuid; - - const GetCeilingDeviceReportsEvent( - {required this.code, required this.deviceUuid}); - - @override - List get props => [code, deviceUuid]; -} - -class ShowCeilingDescriptionEvent extends CeilingSensorEvent { - final String description; - - const ShowCeilingDescriptionEvent({required this.description}); - - @override - List get props => [description]; -} - -class BackToCeilingGridViewEvent extends CeilingSensorEvent {} diff --git a/lib/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart b/lib/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart index e8118168..08a65a11 100644 --- a/lib/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart +++ b/lib/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; class CeilingSensorModel { @@ -10,6 +11,7 @@ class CeilingSensorModel { String bodyMovement; String noBodyTime; int maxDistance; + SpaceTypes spaceType; CeilingSensorModel({ required this.presenceState, @@ -20,6 +22,7 @@ class CeilingSensorModel { required this.bodyMovement, required this.noBodyTime, required this.maxDistance, + required this.spaceType, }); factory CeilingSensorModel.fromJson(List jsonList) { @@ -31,6 +34,7 @@ class CeilingSensorModel { String _bodyMovement = 'none'; String _noBodyTime = 'none'; int _maxDis = 0; + SpaceTypes _spaceType = SpaceTypes.none; try { for (var status in jsonList) { @@ -38,17 +42,23 @@ class CeilingSensorModel { case 'presence_state': _presenceState = status.value ?? 'none'; break; + case 'scene': + _spaceType = getSpaceType(status.value ?? 'none'); + break; case 'sensitivity': - _sensitivity = status.value is int ? status.value : int.tryParse(status.value ?? '1') ?? 1; + _sensitivity = + status.value is int ? status.value : int.tryParse(status.value ?? '1') ?? 1; break; case 'checking_result': _checkingResult = status.value ?? ''; break; case 'presence_range': - _presenceRange = status.value is int ? status.value : int.tryParse(status.value ?? '0') ?? 0; + _presenceRange = + status.value is int ? status.value : int.tryParse(status.value ?? '0') ?? 0; break; case 'sports_para': - _sportsPara = status.value is int ? status.value : int.tryParse(status.value ?? '0') ?? 0; + _sportsPara = + status.value is int ? status.value : int.tryParse(status.value ?? '0') ?? 0; break; case 'body_movement': _bodyMovement = status.value ?? ''; @@ -74,6 +84,55 @@ class CeilingSensorModel { bodyMovement: _bodyMovement, noBodyTime: _noBodyTime, maxDistance: _maxDis, + spaceType: _spaceType, + ); + } + + CeilingSensorModel copyWith({ + String? presenceState, + int? sensitivity, + String? checkingResult, + int? presenceRange, + int? sportsPara, + String? bodyMovement, + String? noBodyTime, + int? maxDistance, + SpaceTypes? spaceType, + }) { + return CeilingSensorModel( + presenceState: presenceState ?? this.presenceState, + sensitivity: sensitivity ?? this.sensitivity, + checkingResult: checkingResult ?? this.checkingResult, + presenceRange: presenceRange ?? this.presenceRange, + sportsPara: sportsPara ?? this.sportsPara, + bodyMovement: bodyMovement ?? this.bodyMovement, + noBodyTime: noBodyTime ?? this.noBodyTime, + maxDistance: maxDistance ?? this.maxDistance, + spaceType: spaceType ?? this.spaceType, ); } } + +enum SpaceTypes { + none, + parlour, + area, + toilet, + bedroom, +} + +SpaceTypes getSpaceType(String value) { + switch (value) { + case 'parlour': + return SpaceTypes.parlour; + case 'area': + return SpaceTypes.area; + case 'toilet': + return SpaceTypes.toilet; + case 'bedroom': + return SpaceTypes.bedroom; + case 'none': + default: + return SpaceTypes.none; + } +} diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart new file mode 100644 index 00000000..b7f9af5d --- /dev/null +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; +import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; +import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiveLayout { + const CeilingSensorBatchControlView({super.key, required this.devicesIds}); + + final List devicesIds; + + @override + Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return BlocProvider( + create: (context) => CeilingSensorBloc(deviceId: devicesIds.first) + ..add(CeilingFetchDeviceStatusEvent(devicesIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is CeilingUpdateState) { + return _buildGridView( + context, state.ceilingSensorModel, isExtraLarge, isLarge, isMedium); + } + return const Center(child: Text('Error fetching status')); + }, + ), + ); + } + + Widget _buildGridView(BuildContext context, CeilingSensorModel model, bool isExtraLarge, + bool isLarge, bool isMedium) { + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + PresenceSpaceType( + description: 'Space Type', + value: model.spaceType, + action: (String value) => context.read().add( + CeilingBatchControlEvent( + deviceIds: devicesIds, + code: 'scene', + value: value, + ), + ), + ), + PresenceUpdateData( + value: model.sensitivity.toDouble(), + title: 'Sensitivity:', + minValue: 1, + maxValue: 10, + steps: 1, + action: (int value) { + context.read().add( + CeilingBatchControlEvent( + deviceIds: devicesIds, + code: 'sensitivity', + value: value, + ), + ); + }, + ), + PresenceUpdateData( + value: model.maxDistance.toDouble(), + title: 'Maximum Distance:', + minValue: 0, + maxValue: 500, + steps: 50, + description: 'm', + action: (int value) => context.read().add( + CeilingBatchControlEvent( + deviceIds: devicesIds, + code: 'moving_max_dis', + value: value, + ), + ), + ), + PresenceNoBodyTime( + value: model.noBodyTime, + title: 'Nobody Time:', + description: '', + action: (String value) => context.read().add( + CeilingBatchControlEvent( + deviceIds: devicesIds, + code: 'nobody_time', + value: value, + ), + ), + ), + FirmwareUpdateWidget(deviceId: devicesIds.first, version: 4), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CeilingFactoryResetEvent( + devicesId: devicesIds.first, + factoryResetModel: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart index 2bb7eb76..845c326b 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/bloc.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/event.dart'; -import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; @@ -16,49 +16,44 @@ import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dar import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class CeilingSensorControls extends StatelessWidget - with HelperResponsiveLayout { - const CeilingSensorControls({super.key, required this.device}); +class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLayout { + const CeilingSensorControlsView({super.key, required this.device}); final AllDevicesModel device; @override Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( create: (context) => CeilingSensorBloc(deviceId: device.uuid ?? '') - ..add(CeilingInitialEvent()), + ..add(CeilingInitialEvent(device.uuid ?? '')), child: BlocBuilder( builder: (context, state) { - if (state is CeilingLoadingInitialState || - state is CeilingReportsLoadingState) { + if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) { return const Center(child: CircularProgressIndicator()); } else if (state is CeilingUpdateState) { return _buildGridView( - context, state.ceilingSensorModel, isLarge, isMedium); + context, state.ceilingSensorModel, isExtraLarge, isLarge, isMedium); } else if (state is CeilingReportsState) { return ReportsTable( report: state.deviceReport, onRowTap: (index) {}, onClose: () { - context - .read() - .add(BackToCeilingGridViewEvent()); + context.read().add(BackToCeilingGridViewEvent()); }, ); } else if (state is ShowCeilingDescriptionState) { return DescriptionView( description: state.description, onClose: () { - context - .read() - .add(BackToCeilingGridViewEvent()); + context.read().add(BackToCeilingGridViewEvent()); }, ); } else if (state is CeilingReportsFailedState) { final model = context.read().deviceStatus; - return _buildGridView(context, model, isLarge, isMedium); + return _buildGridView(context, model, isExtraLarge, isLarge, isMedium); } return const Center(child: Text('Error fetching status')); }, @@ -66,14 +61,14 @@ class CeilingSensorControls extends StatelessWidget ); } - Widget _buildGridView(BuildContext context, CeilingSensorModel model, + Widget _buildGridView(BuildContext context, CeilingSensorModel model, bool isExtraLarge, bool isLarge, bool isMedium) { return GridView( padding: const EdgeInsets.symmetric(horizontal: 50), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isLarge + crossAxisCount: isLarge || isExtraLarge ? 3 : isMedium ? 2 @@ -96,21 +91,21 @@ class CeilingSensorControls extends StatelessWidget postfix: 'm', description: 'Detection Range', ), - const PresenceSpaceType( - listOfIcons: [ - Assets.office, - Assets.parlour, - Assets.dyi, - Assets.bathroom, - Assets.bedroom, - ], + PresenceSpaceType( description: 'Space Type', + value: model.spaceType, + action: (String value) => context.read().add( + CeilingChangeValueEvent( + code: 'scene', + value: value, + ), + ), ), PresenceUpdateData( value: model.sensitivity.toDouble(), title: 'Sensitivity:', minValue: 1, - maxValue: 5, + maxValue: 10, steps: 1, action: (int value) { context.read().add( @@ -138,7 +133,7 @@ class CeilingSensorControls extends StatelessWidget PresenceNoBodyTime( value: model.noBodyTime, title: 'Nobody Time:', - // description: 'hr', + description: '', action: (String value) => context.read().add( CeilingChangeValueEvent( code: 'nobody_time', @@ -148,8 +143,8 @@ class CeilingSensorControls extends StatelessWidget ), GestureDetector( onTap: () { - context.read().add(GetCeilingDeviceReportsEvent( - code: 'presence_state', deviceUuid: device.uuid!)); + context.read().add( + GetCeilingDeviceReportsEvent(code: 'presence_state', deviceUuid: device.uuid!)); }, child: const PresenceStaticWidget( icon: Assets.illuminanceRecordIcon, @@ -158,8 +153,9 @@ class CeilingSensorControls extends StatelessWidget ), GestureDetector( onTap: () { - context.read().add(GetCeilingDeviceReportsEvent( - code: '', deviceUuid: device.uuid!)); + context + .read() + .add(GetCeilingDeviceReportsEvent(code: '', deviceUuid: device.uuid!)); }, child: const PresenceStaticWidget( icon: Assets.helpDescriptionIcon, diff --git a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart new file mode 100644 index 00000000..4599f360 --- /dev/null +++ b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class CurtainBloc extends Bloc { + late bool deviceStatus; + final String deviceId; + Timer? _timer; + + CurtainBloc({required this.deviceId}) : super(CurtainInitial()) { + on(_onFetchDeviceStatus); + on(_onFetchBatchStatus); + on(_onCurtainControl); + on(_onCurtainBatchControl); + on(_onFactoryReset); + } + + FutureOr _onFetchDeviceStatus( + CurtainFetchDeviceStatus event, Emitter emit) async { + emit(CurtainStatusLoading()); + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + + deviceStatus = _checkStatus(status.status[0].value); + + emit(CurtainStatusLoaded(deviceStatus)); + } catch (e) { + emit(CurtainError(e.toString())); + } + } + + FutureOr _onCurtainControl( + CurtainControl event, Emitter emit) async { + final oldValue = deviceStatus; + + _updateLocalValue(event.value, emit); + + emit(CurtainStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(const Duration(seconds: 1), () async { + try { + final controlValue = value ? 'open' : 'close'; + + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, controlValue); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: controlValue)); + } + + if (!response) { + _revertValueAndEmit(id, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, oldValue, emit); + } + }); + } + + void _revertValueAndEmit( + String deviceId, bool oldValue, Emitter emit) { + _updateLocalValue(oldValue, emit); + emit(CurtainStatusLoaded(deviceStatus)); + emit(const CurtainControlError('Failed to control the device.')); + } + + void _updateLocalValue(bool value, Emitter emit) { + deviceStatus = value; + emit(CurtainStatusLoaded(deviceStatus)); + } + + bool _checkStatus(String command) { + return command.toLowerCase() == 'open'; + } + + FutureOr _onFetchBatchStatus( + CurtainFetchBatchStatus event, Emitter emit) async { + emit(CurtainStatusLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + + deviceStatus = _checkStatus(status.status[0].value); + + emit(CurtainStatusLoaded(deviceStatus)); + } catch (e) { + emit(CurtainError(e.toString())); + } + } + + FutureOr _onCurtainBatchControl( + CurtainBatchControl event, Emitter emit) async { + final oldValue = deviceStatus; + + _updateLocalValue(event.value, emit); + + emit(CurtainStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.devicesIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + FutureOr _onFactoryReset( + CurtainFactoryReset event, Emitter emit) async { + emit(CurtainStatusLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const CurtainControlError('Failed')); + } else { + add(CurtainFetchDeviceStatus(event.deviceId)); + } + } catch (e) { + emit(CurtainControlError(e.toString())); + } + } +} diff --git a/lib/pages/device_managment/curtain/bloc/curtain_event.dart b/lib/pages/device_managment/curtain/bloc/curtain_event.dart new file mode 100644 index 00000000..7236016c --- /dev/null +++ b/lib/pages/device_managment/curtain/bloc/curtain_event.dart @@ -0,0 +1,62 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +sealed class CurtainEvent extends Equatable { + const CurtainEvent(); + + @override + List get props => []; +} + +class CurtainFetchDeviceStatus extends CurtainEvent { + final String deviceId; + + const CurtainFetchDeviceStatus(this.deviceId); + + @override + List get props => [deviceId]; +} + +class CurtainFetchBatchStatus extends CurtainEvent { + final List devicesIds; + + const CurtainFetchBatchStatus(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class CurtainControl extends CurtainEvent { + final String deviceId; + final String code; + final bool value; + + const CurtainControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class CurtainBatchControl extends CurtainEvent { + final List devicesIds; + final String code; + final bool value; + + const CurtainBatchControl( + {required this.devicesIds, required this.code, required this.value}); + + @override + List get props => [devicesIds, code, value]; +} + +class CurtainFactoryReset extends CurtainEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const CurtainFactoryReset( + {required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/curtain/bloc/curtain_state.dart b/lib/pages/device_managment/curtain/bloc/curtain_state.dart new file mode 100644 index 00000000..dfe11c2a --- /dev/null +++ b/lib/pages/device_managment/curtain/bloc/curtain_state.dart @@ -0,0 +1,40 @@ + +import 'package:equatable/equatable.dart'; + +sealed class CurtainState extends Equatable { + const CurtainState(); + + @override + List get props => []; +} + +final class CurtainInitial extends CurtainState {} + +class CurtainStatusLoading extends CurtainState {} + +class CurtainStatusLoaded extends CurtainState { + final bool status; + + const CurtainStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class CurtainError extends CurtainState { + final String message; + + const CurtainError(this.message); + + @override + List get props => [message]; +} + +class CurtainControlError extends CurtainState { + final String message; + + const CurtainControlError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/pages/device_managment/curtain/model/curtain_model.dart b/lib/pages/device_managment/curtain/model/curtain_model.dart new file mode 100644 index 00000000..908415d5 --- /dev/null +++ b/lib/pages/device_managment/curtain/model/curtain_model.dart @@ -0,0 +1,32 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class CurtainModel { + final String productUuid; + final String productType; + final List status; + + CurtainModel({ + required this.productUuid, + required this.productType, + required this.status, + }); + + factory CurtainModel.fromJson(dynamic json) { + var statusList = json['status'] as List; + List status = statusList.map((i) => Status.fromJson(i)).toList(); + + return CurtainModel( + productUuid: json['productUuid'], + productType: json['productType'], + status: status, + ); + } + + Map toJson() { + return { + 'productUuid': productUuid, + 'productType': productType, + 'status': status.map((s) => s.toJson()).toList(), + }; + } +} diff --git a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart new file mode 100644 index 00000000..b558c837 --- /dev/null +++ b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/curtain_toggle.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class CurtainBatchStatusView extends StatelessWidget + with HelperResponsiveLayout { + const CurtainBatchStatusView({super.key, required this.devicesIds}); + + final List devicesIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainBloc(deviceId: devicesIds.first) + ..add(CurtainFetchBatchStatus(devicesIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is CurtainStatusLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is CurtainStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is CurtainError || state is CurtainControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, bool status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + CurtainToggle( + value: status, + code: 'control', + deviceId: devicesIds.first, + label: 'Curtains', + onChanged: (value) { + context.read().add(CurtainBatchControl( + devicesIds: devicesIds, + code: 'control', + value: value, + )); + }, + ), + FirmwareUpdateWidget(deviceId: devicesIds.first, version: 5), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CurtainFactoryReset( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain/view/curtain_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_status_view.dart new file mode 100644 index 00000000..2afe49f4 --- /dev/null +++ b/lib/pages/device_managment/curtain/view/curtain_status_view.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/curtain_toggle.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class CurtainStatusControlsView extends StatelessWidget + with HelperResponsiveLayout { + final String deviceId; + + const CurtainStatusControlsView({super.key, required this.deviceId}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainBloc(deviceId: deviceId) + ..add(CurtainFetchDeviceStatus(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is CurtainStatusLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is CurtainStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is CurtainError || state is CurtainControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, bool status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + const SizedBox.shrink(), + CurtainToggle( + value: status, + code: 'control', + deviceId: deviceId, + label: 'Curtains', + onChanged: (value) { + context.read().add( + CurtainControl( + deviceId: deviceId, + code: 'control', + value: value, + ), + ); + }, + ), + const SizedBox.shrink(), + ], + ); + } +} diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart index 8ad2a05c..c50203f3 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart @@ -17,6 +17,7 @@ class DoorLockBloc extends Bloc { on(_onFetchDeviceStatus); //on(_onDoorLockControl); on(_updateLock); + on(_onFactoryReset); } FutureOr _onFetchDeviceStatus( @@ -113,4 +114,22 @@ class DoorLockBloc extends Bloc { return null; } } + + FutureOr _onFactoryReset( + DoorLockFactoryReset event, Emitter emit) async { + emit(DoorLockStatusLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const DoorLockControlError('Failed')); + } else { + add(DoorLockFetchStatus(event.deviceId)); + } + } catch (e) { + emit(DoorLockControlError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart index 8ee2e6aa..54fa1ddf 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; sealed class DoorLockEvent extends Equatable { const DoorLockEvent(); @@ -37,3 +38,16 @@ class UpdateLockEvent extends DoorLockEvent { @override List get props => [value]; } + +class DoorLockFactoryReset extends DoorLockEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const DoorLockFactoryReset({ + required this.deviceId, + required this.factoryReset, + }); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart b/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart new file mode 100644 index 00000000..abbd48dd --- /dev/null +++ b/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/door_lock/bloc/door_lock_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/door_lock/bloc/door_lock_event.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class DoorLockBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + const DoorLockBatchControlView({super.key, required this.devicesIds}); + + final List devicesIds; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + height: 140, + child: FirmwareUpdateWidget( + deviceId: devicesIds.first, + version: 12, + ), + ), + const SizedBox( + width: 12, + ), + SizedBox( + width: 170, + height: 140, + child: FactoryResetWidget( + callFactoryReset: () { + BlocProvider.of(context).add( + DoorLockFactoryReset( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/door_lock/view/door_lock_status_view.dart b/lib/pages/device_managment/door_lock/view/door_lock_control_view.dart similarity index 90% rename from lib/pages/device_managment/door_lock/view/door_lock_status_view.dart rename to lib/pages/device_managment/door_lock/view/door_lock_control_view.dart index 12db871f..346da6bf 100644 --- a/lib/pages/device_managment/door_lock/view/door_lock_status_view.dart +++ b/lib/pages/device_managment/door_lock/view/door_lock_control_view.dart @@ -7,10 +7,10 @@ import 'package:syncrow_web/pages/device_managment/door_lock/bloc/door_lock_stat import 'package:syncrow_web/pages/device_managment/door_lock/models/door_lock_status_model.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/widget/door_button.dart'; -class DoorLockView extends StatelessWidget { +class DoorLockControlsView extends StatelessWidget { final AllDevicesModel device; - const DoorLockView({super.key, required this.device}); + const DoorLockControlsView({super.key, required this.device}); @override Widget build(BuildContext context) { @@ -34,9 +34,9 @@ class DoorLockView extends StatelessWidget { } else if (state is UpdateState) { return _buildStatusControls(context, state.smartDoorModel); } else if (state is DoorLockControlError) { - return Center(child: Text(state.message)); + return const SizedBox(); } else { - return const Center(child: CircularProgressIndicator()); + return const Center(child: Text('Error fetching status')); } }, ), diff --git a/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart b/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart new file mode 100644 index 00000000..7060c668 --- /dev/null +++ b/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart @@ -0,0 +1,423 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; + +class GarageDoorBloc extends Bloc { + final String deviceId; + late GarageDoorStatusModel deviceStatus; + Timer? _timer; + + GarageDoorBloc({required this.deviceId}) : super(GarageDoorInitialState()) { + on(_fetchGarageDoorStatus); + on(_garageDoorControlEvent); + on(_addSchedule); + on(_updateSchedule); + on(_deleteSchedule); + on(_fetchSchedules); + on(_increaseDelay); + on(_decreaseDelay); + on(_fetchRecords); + on(_handleUpdate); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); + on(_initializeAddSchedule); + on(_backToGridView); + on(_onUpdateCountdownAlarm); + on(_onUpdateTrTimeCon); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFactoryReset); + on(_onEditSchedule); + } + + void _fetchGarageDoorStatus(GarageDoorInitialEvent event, Emitter emit) async { + emit(GarageDoorLoadingState()); + try { + var response = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = GarageDoorStatusModel.fromJson(deviceId, response.status); + emit(GarageDoorLoadedState(status: deviceStatus)); + } catch (e) { + emit(GarageDoorErrorState(message: e.toString())); + } + } + + Future _onFetchBatchStatus(GarageDoorFetchBatchStatusEvent event, Emitter emit) async { + emit(GarageDoorLoadingState()); + try { + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = GarageDoorStatusModel.fromJson(event.deviceIds.first, status.status); + emit(GarageDoorBatchStatusLoaded(deviceStatus)); + } catch (e) { + emit(GarageDoorBatchControlError(e.toString())); + } + } + + Future _addSchedule(AddGarageDoorScheduleEvent event, Emitter emit) async { + try { + ScheduleEntry newSchedule = ScheduleEntry( + category: event.category, + time: formatTimeOfDayToISO(event.time), + function: Status(code: 'switch_1', value: event.functionOn), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), + ); + bool success = await DevicesManagementApi().addScheduleRecord(newSchedule, deviceId); + if (success) { + add(FetchGarageDoorSchedulesEvent(deviceId: deviceId, category: 'switch_1')); + } else { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } catch (e) { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } + + void _onUpdateCountdownAlarm(UpdateCountdownAlarmEvent event, Emitter emit) { + final currentState = state; + if (currentState is GarageDoorLoadedState) { + emit(currentState.copyWith( + status: currentState.status.copyWith(countdownAlarm: event.countdownAlarm), + )); + } + } + + void _onUpdateTrTimeCon(UpdateTrTimeConEvent event, Emitter emit) { + final currentState = state; + if (currentState is GarageDoorLoadedState) { + emit(currentState.copyWith( + status: currentState.status.copyWith(trTimeCon: event.trTimeCon), + )); + } + } + + Future _updateSchedule(UpdateGarageDoorScheduleEvent event, Emitter emit) async { + try { + final updatedSchedules = deviceStatus.schedules?.map((schedule) { + if (schedule.scheduleId == event.scheduleId) { + return schedule.copyWith( + function: Status(code: 'switch_1', value: event.functionOn), + enable: event.enable, + ); + } + return schedule; + }).toList(); + bool success = await DevicesManagementApi().updateScheduleRecord( + enable: event.enable, + uuid: deviceStatus.uuid, + scheduleId: event.scheduleId, + ); + if (success) { + deviceStatus = deviceStatus.copyWith(schedules: updatedSchedules); + emit(GarageDoorLoadedState(status: deviceStatus)); + } else { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } catch (e) { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } + + Future _deleteSchedule(DeleteGarageDoorScheduleEvent event, Emitter emit) async { + try { + bool success = await DevicesManagementApi().deleteScheduleRecord(deviceStatus.uuid, event.scheduleId); + if (success) { + final updatedSchedules = + deviceStatus.schedules?.where((schedule) => schedule.scheduleId != event.scheduleId).toList(); + deviceStatus = deviceStatus.copyWith(schedules: updatedSchedules); + emit(GarageDoorLoadedState(status: deviceStatus)); + } else { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } catch (e) { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } + + Future _fetchSchedules(FetchGarageDoorSchedulesEvent event, Emitter emit) async { + emit(ScheduleGarageLoadingState()); + try { + List schedules = + await DevicesManagementApi().getDeviceSchedules(deviceStatus.uuid, event.category); + deviceStatus = deviceStatus.copyWith(schedules: schedules); + emit( + GarageDoorLoadedState( + status: deviceStatus, + scheduleMode: ScheduleModes.schedule, + ), + ); + } catch (e) { + emit( + GarageDoorLoadedState( + status: deviceStatus, + scheduleMode: ScheduleModes.schedule, + ), + ); + } + } + + Future _updateSelectedTime(UpdateSelectedTimeEvent event, Emitter emit) async { + final currentState = state; + if (currentState is GarageDoorLoadedState) { + emit(currentState.copyWith(selectedTime: event.selectedTime)); + } + } + + Future _updateSelectedDay(UpdateSelectedDayEvent event, Emitter emit) async { + final currentState = state; + if (currentState is GarageDoorLoadedState) { + List updatedDays = List.from(currentState.selectedDays); + updatedDays[event.dayIndex] = event.isSelected; + emit(currentState.copyWith(selectedDays: updatedDays, selectedTime: currentState.selectedTime)); + } + } + + Future _updateFunctionOn(UpdateFunctionOnEvent event, Emitter emit) async { + final currentState = state; + if (currentState is GarageDoorLoadedState) { + emit(currentState.copyWith(functionOn: event.functionOn, selectedTime: currentState.selectedTime)); + } + } + + Future _initializeAddSchedule(InitializeAddScheduleEvent event, Emitter emit) async { + final currentState = state; + if (currentState is GarageDoorLoadedState) { + emit(currentState.copyWith( + selectedTime: event.selectedTime, + selectedDays: event.selectedDays, + functionOn: event.functionOn, + isEditing: event.isEditing, + )); + } + } + + Future _fetchRecords(FetchGarageDoorRecordsEvent event, Emitter emit) async { + emit(GarageDoorReportsLoadingState()); + try { + final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; + final to = DateTime.now().millisecondsSinceEpoch; + final DeviceReport records = + await DevicesManagementApi.getDeviceReportsByDate(event.deviceId, 'switch_1', from.toString(), to.toString()); + emit(GarageDoorReportsState(deviceReport: records)); + } catch (e) { + emit(GarageDoorReportsFailedState(error: e.toString())); + } + } + + Future _onBatchControl(GarageDoorBatchControlEvent event, Emitter emit) async { + final oldValue = event.code == 'switch_1' ? deviceStatus.switch1 : false; + + _updateLocalValue(event.code, event.value); + emit(GarageDoorBatchStatusLoaded(deviceStatus)); + + final success = await _runDeBouncer( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + + if (!success) { + _revertValue(event.code, oldValue, emit); + } + } + + void _backToGridView(BackToGarageDoorGridViewEvent event, Emitter emit) { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + + void _handleUpdate(GarageDoorUpdatedEvent event, Emitter emit) { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + + Future _runDeBouncer({ + required dynamic deviceId, + required String code, + required dynamic value, + required dynamic oldValue, + required Emitter emit, + required bool isBatch, + }) async { + try { + late bool status; + await Future.delayed(const Duration(milliseconds: 500)); + if (isBatch) { + status = await DevicesManagementApi().deviceBatchControl(deviceId, code, value); + } else { + status = await DevicesManagementApi().deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!status) { + _revertValue(code, oldValue, emit); + return false; + } else { + return true; + } + } catch (e) { + _revertValue(code, oldValue, emit); + return false; + } + } + + Future _onFactoryReset(GarageDoorFactoryResetEvent event, Emitter emit) async { + emit(GarageDoorLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset(event.factoryReset, event.deviceId); + if (!response) { + emit(const GarageDoorErrorState(message: 'Failed to reset device')); + } else { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } catch (e) { + emit(GarageDoorErrorState(message: e.toString())); + } + } + + void _increaseDelay(IncreaseGarageDoorDelayEvent event, Emitter emit) async { + // if (deviceStatus.countdown1 != 0) { + try { + deviceStatus = deviceStatus.copyWith(delay: deviceStatus.delay + Duration(minutes: 10)); + emit(GarageDoorLoadedState(status: deviceStatus)); + add(GarageDoorControlEvent(deviceId: event.deviceId, value: deviceStatus.delay.inSeconds, code: 'countdown_1')); + } catch (e) { + emit(GarageDoorErrorState(message: e.toString())); + } + // } + } + + void _decreaseDelay(DecreaseGarageDoorDelayEvent event, Emitter emit) async { + // if (deviceStatus.countdown1 != 0) { + try { + if (deviceStatus.delay.inMinutes > 10) { + deviceStatus = deviceStatus.copyWith(delay: deviceStatus.delay - Duration(minutes: 10)); + } + emit(GarageDoorLoadedState(status: deviceStatus)); + add(GarageDoorControlEvent(deviceId: event.deviceId, value: deviceStatus.delay.inSeconds, code: 'countdown_1')); + } catch (e) { + emit(GarageDoorErrorState(message: e.toString())); + } + //} + } + + void _garageDoorControlEvent(GarageDoorControlEvent event, Emitter emit) async { + final oldValue = event.code == 'countdown_1' ? deviceStatus.countdown1 : deviceStatus.switch1; + _updateLocalValue(event.code, event.value); + emit(GarageDoorLoadedState(status: deviceStatus)); + final success = await _runDeBouncer( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + if (!success) { + _revertValue(event.code, oldValue, emit); + } + } + + void _revertValue(String code, dynamic oldValue, Emitter emit) { + switch (code) { + case 'switch_1': + if (oldValue is bool) { + deviceStatus = deviceStatus.copyWith(switch1: oldValue); + } + break; + case 'countdown_1': + if (oldValue is int) { + deviceStatus = deviceStatus.copyWith(countdown1: oldValue, delay: Duration(seconds: oldValue)); + } + break; + // Add other cases if needed + default: + break; + } + if (state is GarageDoorLoadedState) { + final currentState = state as GarageDoorLoadedState; + emit(currentState.copyWith(status: deviceStatus)); + } + } + + void _updateLocalValue(String code, dynamic value) { + switch (code) { + case 'switch_1': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + break; + case 'countdown_1': + if (value is int) { + deviceStatus = deviceStatus.copyWith(countdown1: value, delay: Duration(seconds: value)); + } + break; + case 'countdown_alarm': + if (value is int) { + deviceStatus = deviceStatus.copyWith(countdownAlarm: value); + } + break; + case 'tr_timecon': + if (value is int) { + deviceStatus = deviceStatus.copyWith(trTimeCon: value); + } + break; + case 'door_state_1': + if (value is String) { + deviceStatus = deviceStatus.copyWith(doorState1: value); + } + break; + case 'door_control_1': + if (value is String) { + deviceStatus = deviceStatus.copyWith(doorControl1: value); + } + break; + case 'voice_control_1': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(voiceControl1: value); + } + break; + case 'door_contact_state': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(doorContactState: value); + } + default: + break; + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } + + FutureOr _onEditSchedule(EditGarageDoorScheduleEvent event, Emitter emit) async { + try { + ScheduleEntry newSchedule = ScheduleEntry( + scheduleId: event.scheduleId, + category: event.category, + time: formatTimeOfDayToISO(event.time), + function: Status(code: 'switch_1', value: event.functionOn), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), + ); + bool success = await DevicesManagementApi().editScheduleRecord(deviceId, newSchedule); + if (success) { + add(FetchGarageDoorSchedulesEvent(deviceId: deviceId, category: 'switch_1')); + } else { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } catch (e) { + emit(GarageDoorLoadedState(status: deviceStatus)); + } + } +} diff --git a/lib/pages/device_managment/garage_door/bloc/garage_door_event.dart b/lib/pages/device_managment/garage_door/bloc/garage_door_event.dart new file mode 100644 index 00000000..d1fb15bb --- /dev/null +++ b/lib/pages/device_managment/garage_door/bloc/garage_door_event.dart @@ -0,0 +1,234 @@ +// lib/pages/device_managment/garage_door/bloc/event.dart + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +abstract class GarageDoorEvent extends Equatable { + const GarageDoorEvent(); + + @override + List get props => []; +} + +class GarageDoorInitialEvent extends GarageDoorEvent { + final String deviceId; + + const GarageDoorInitialEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class GarageDoorControlEvent extends GarageDoorEvent { + final String deviceId; + final dynamic value; + final String code; + + const GarageDoorControlEvent({required this.deviceId, required this.value, required this.code}); + + @override + List get props => [deviceId, value]; +} + +class AddGarageDoorScheduleEvent extends GarageDoorEvent { + final String category; + final TimeOfDay time; + final bool functionOn; + final List selectedDays; + + const AddGarageDoorScheduleEvent({ + required this.category, + required this.time, + required this.functionOn, + required this.selectedDays, + }); +} + +class EditGarageDoorScheduleEvent extends GarageDoorEvent { + final String scheduleId; + final String category; + final TimeOfDay time; + final bool functionOn; + final List selectedDays; + + const EditGarageDoorScheduleEvent({ + required this.scheduleId, + required this.category, + required this.time, + required this.functionOn, + required this.selectedDays, + }); +} + +class UpdateGarageDoorScheduleEvent extends GarageDoorEvent { + final String deviceId; + final String scheduleId; + final bool enable; + final bool functionOn; + final int index; + + const UpdateGarageDoorScheduleEvent({ + required this.deviceId, + required this.scheduleId, + required this.enable, + required this.functionOn, + required this.index, + }); +} + +class DeleteGarageDoorScheduleEvent extends GarageDoorEvent { + final String deviceId; + final String scheduleId; + final int index; + + const DeleteGarageDoorScheduleEvent({ + required this.deviceId, + required this.scheduleId, + required this.index, + }); +} + +class FetchGarageDoorSchedulesEvent extends GarageDoorEvent { + final String deviceId; + final String category; + + const FetchGarageDoorSchedulesEvent({ + required this.deviceId, + required this.category, + }); +} + +class IncreaseGarageDoorDelayEvent extends GarageDoorEvent { + final String deviceId; + + const IncreaseGarageDoorDelayEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class DecreaseGarageDoorDelayEvent extends GarageDoorEvent { + final String deviceId; + + const DecreaseGarageDoorDelayEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class FetchGarageDoorRecordsEvent extends GarageDoorEvent { + final String deviceId; + final String code; + + const FetchGarageDoorRecordsEvent({required this.deviceId, required this.code}); + + @override + List get props => [deviceId, code]; +} + +class BackToGarageDoorGridViewEvent extends GarageDoorEvent {} + +class GarageDoorUpdatedEvent extends GarageDoorEvent {} + +class UpdateSelectedTimeEvent extends GarageDoorEvent { + final TimeOfDay? selectedTime; + + const UpdateSelectedTimeEvent(this.selectedTime); + + @override + List get props => [selectedTime]; +} + +class UpdateSelectedDayEvent extends GarageDoorEvent { + final int dayIndex; + final bool isSelected; + + const UpdateSelectedDayEvent(this.dayIndex, this.isSelected); + + @override + List get props => [dayIndex, isSelected]; +} + +class UpdateFunctionOnEvent extends GarageDoorEvent { + final bool functionOn; + + const UpdateFunctionOnEvent({required this.functionOn}); + + @override + List get props => [functionOn]; +} + +class InitializeAddScheduleEvent extends GarageDoorEvent { + final TimeOfDay? selectedTime; + final List selectedDays; + final bool functionOn; + final bool isEditing; + final int? index; + + const InitializeAddScheduleEvent({ + required this.selectedTime, + required this.selectedDays, + required this.functionOn, + required this.isEditing, + this.index, + }); + + @override + List get props => [ + selectedTime, + selectedDays, + functionOn, + isEditing, + index, + ]; +} + +class UpdateCountdownAlarmEvent extends GarageDoorEvent { + final int countdownAlarm; + + const UpdateCountdownAlarmEvent(this.countdownAlarm); +} + +class UpdateTrTimeConEvent extends GarageDoorEvent { + final int trTimeCon; + + const UpdateTrTimeConEvent(this.trTimeCon); +} + +class GarageDoorBatchControlEvent extends GarageDoorEvent { + final List deviceIds; + final String code; + final bool value; + + const GarageDoorBatchControlEvent({ + required this.deviceIds, + required this.code, + required this.value, + }); + + @override + List get props => [deviceIds, code, value]; +} + +class GarageDoorFetchBatchStatusEvent extends GarageDoorEvent { + final List deviceIds; + + const GarageDoorFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; +} + +class GarageDoorFactoryResetEvent extends GarageDoorEvent { + final FactoryResetModel factoryReset; + final String deviceId; + + const GarageDoorFactoryResetEvent({ + required this.factoryReset, + required this.deviceId, + }); + + @override + List get props => [factoryReset, deviceId]; +} diff --git a/lib/pages/device_managment/garage_door/bloc/garage_door_state.dart b/lib/pages/device_managment/garage_door/bloc/garage_door_state.dart new file mode 100644 index 00000000..2b63a3f8 --- /dev/null +++ b/lib/pages/device_managment/garage_door/bloc/garage_door_state.dart @@ -0,0 +1,141 @@ +// lib/pages/device_managment/garage_door/bloc/state.dart + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; + +abstract class GarageDoorState extends Equatable { + const GarageDoorState(); + + @override + List get props => []; +} + +class GarageDoorInitialState extends GarageDoorState {} + +class GarageDoorLoadingState extends GarageDoorState {} + +class GarageDoorLoadedState extends GarageDoorState { + final GarageDoorStatusModel status; + final Duration? delay; + final DeviceReport? records; + final List selectedDays; + final TimeOfDay? selectedTime; + final bool functionOn; + final bool isEditing; + final ScheduleModes? scheduleMode; + + const GarageDoorLoadedState({ + required this.status, + this.delay, + this.records, + this.selectedDays = const [false, false, false, false, false, false, false], + this.selectedTime, + this.functionOn = false, + this.isEditing = false, + this.scheduleMode = ScheduleModes.schedule, + }); + + @override + List get props => [ + status, + delay, + records, + selectedDays, + selectedTime, + functionOn, + isEditing, + scheduleMode, + ]; + + GarageDoorLoadedState copyWith({ + GarageDoorStatusModel? status, + Duration? delay, + DeviceReport? records, + List? selectedDays, + TimeOfDay? selectedTime, + bool? functionOn, + bool? isEditing, + ScheduleModes? scheduleMode, + }) { + return GarageDoorLoadedState( + status: status ?? this.status, + delay: delay ?? this.delay, + records: records ?? this.records, + selectedDays: selectedDays ?? this.selectedDays, + selectedTime: selectedTime, + functionOn: functionOn ?? this.functionOn, + isEditing: isEditing ?? this.isEditing, + scheduleMode: scheduleMode ?? this.scheduleMode, + ); + } +} + +class GarageDoorErrorState extends GarageDoorState { + final String message; + + const GarageDoorErrorState({required this.message}); + + @override + List get props => [message]; +} + +class GarageDoorLoadingNewState extends GarageDoorState { + final GarageDoorStatusModel garageDoorModel; + + const GarageDoorLoadingNewState({required this.garageDoorModel}); + + @override + List get props => [garageDoorModel]; +} + +class GarageDoorReportsLoadingState extends GarageDoorState {} + +class GarageDoorReportsFailedState extends GarageDoorState { + final String error; + + const GarageDoorReportsFailedState({required this.error}); + + @override + List get props => [error]; +} + +class GarageDoorReportsState extends GarageDoorState { + final DeviceReport deviceReport; + + const GarageDoorReportsState({required this.deviceReport}); + + @override + List get props => [deviceReport]; +} + +class ShowGarageDoorDescriptionState extends GarageDoorState { + final String description; + + const ShowGarageDoorDescriptionState({required this.description}); + + @override + List get props => [description]; +} + +class ScheduleGarageLoadingState extends GarageDoorState {} + +class GarageDoorBatchStatusLoaded extends GarageDoorState { + final GarageDoorStatusModel status; + + const GarageDoorBatchStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class GarageDoorBatchControlError extends GarageDoorState { + final String message; + + const GarageDoorBatchControlError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart new file mode 100644 index 00000000..7b133d45 --- /dev/null +++ b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart @@ -0,0 +1,385 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart'; +import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class GarageDoorDialogHelper { + static void showAddGarageDoorScheduleDialog(BuildContext context, + {ScheduleModel? schedule, int? index, bool? isEdit}) { + final bloc = context.read(); + + if (schedule == null) { + bloc.add((const UpdateSelectedTimeEvent(null))); + bloc.add(InitializeAddScheduleEvent( + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + )); + } else { + final time = _convertStringToTimeOfDay(schedule.time); + final selectedDays = _convertDaysStringToBooleans(schedule.days); + + bloc.add(InitializeAddScheduleEvent( + selectedTime: time, + selectedDays: selectedDays, + functionOn: schedule.function.value, + isEditing: true, + index: index, + )); + } + + showDialog( + context: context, + builder: (ctx) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + if (state is GarageDoorLoadedState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: DefaultButton( + padding: 8, + backgroundColor: ColorsManager.boxColor, + borderRadius: 15, + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: state.selectedTime ?? TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + ), + ), + child: child!, + ); + }, + ); + if (time != null) { + bloc.add(UpdateSelectedTimeEvent(time)); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + state.selectedTime == null ? 'Time' : state.selectedTime!.format(context), + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const Icon( + Icons.access_time, + color: ColorsManager.grayColor, + size: 18, + ), + ], + ), + ), + ), + const SizedBox(height: 16), + _buildDayCheckboxes(context, state.selectedDays, isEdit: isEdit), + const SizedBox(height: 16), + _buildFunctionSwitch(context, state.functionOn, isEdit), + ], + ), + actions: [ + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + if (state.selectedTime != null) { + if (state.isEditing && index != null) { + bloc.add(EditGarageDoorScheduleEvent( + scheduleId: schedule?.scheduleId ?? '', + category: 'switch_1', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } else { + bloc.add(AddGarageDoorScheduleEvent( + category: 'switch_1', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } + Navigator.pop(context); + } + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } + return const SizedBox(); + }, + ), + ); + }, + ); + } + + static TimeOfDay _convertStringToTimeOfDay(String timeString) { + final regex = RegExp(r'^(\d{2}):(\d{2})$'); + final match = regex.firstMatch(timeString); + if (match != null) { + final hour = int.parse(match.group(1)!); + final minute = int.parse(match.group(2)!); + return TimeOfDay(hour: hour, minute: minute); + } else { + throw const FormatException('Invalid time format'); + } + } + + static List _convertDaysStringToBooleans(List selectedDays) { + final daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List daysBoolean = List.filled(7, false); + + for (int i = 0; i < daysOfWeek.length; i++) { + if (selectedDays.contains(daysOfWeek[i])) { + daysBoolean[i] = true; + } + } + + return daysBoolean; + } + + static Widget _buildDayCheckboxes(BuildContext context, List selectedDays, {bool? isEdit}) { + final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + return Row( + children: List.generate(7, (index) { + return Row( + children: [ + Checkbox( + value: selectedDays[index], + onChanged: (bool? value) { + context.read().add(UpdateSelectedDayEvent(index, value!)); + }, + ), + Text(dayLabels[index]), + ], + ); + }), + ); + } + + static Widget _buildFunctionSwitch(BuildContext context, bool isOn, bool? isEdit) { + return Row( + children: [ + Text( + 'Function:', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.grayColor), + ), + const SizedBox(width: 10), + Radio( + value: true, + groupValue: isOn, + onChanged: (bool? value) { + context.read().add(const UpdateFunctionOnEvent(functionOn: true)); + }, + ), + const Text('On'), + const SizedBox(width: 10), + Radio( + value: false, + groupValue: isOn, + onChanged: (bool? value) { + context.read().add(const UpdateFunctionOnEvent(functionOn: false)); + }, + ), + const Text('Off'), + ], + ); + } + + static void showPreferencesDialog(BuildContext context) { + final bloc = context.read(); + + showDialog( + context: context, + builder: (ctx) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + if (state is GarageDoorLoadedState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + + /// The dialog is closed when the user taps on the close button or when the + /// [GarageDoorBloc] state changes. + Text( + 'Preferences', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + const SizedBox(width: 24), + SizedBox( + width: 190, + height: 150, + child: GestureDetector( + onTap: () { + context.customAlertDialog( + alertBody: TimeOutAlarmDialogBody(bloc), + title: 'Time Out Alarm', + onConfirm: () { + final updatedState = context.read().state; + if (updatedState is GarageDoorLoadedState) { + context.read().add( + GarageDoorControlEvent( + deviceId: updatedState.status.uuid, + code: 'countdown_alarm', + value: updatedState.status.countdownAlarm, + ), + ); + Navigator.pop(context); + } + }); + }, + child: ToggleWidget( + icon: "-1", + value: state.status.doorState1 == "close_time_alarm" ? false : true, + code: 'door_state_1', + deviceId: bloc.deviceId, + label: 'Alarm when door is open', + onChange: (value) { + context.read().add( + GarageDoorControlEvent( + deviceId: bloc.deviceId, + code: 'door_state_1', + value: state.status.doorState1 == "close_time_alarm" + ? "unclosed_time" + : "close_time_alarm", + ), + ); + }), + ), + ), + const SizedBox( + width: 20, + ), + SizedBox( + width: 190, + height: 150, + child: GestureDetector( + onTap: () { + context.customAlertDialog( + alertBody: OpeningAndClosingTimeDialogBody( + bloc: bloc, + onDurationChanged: (newDuration) { + context.read().add( + UpdateTrTimeConEvent(newDuration), + ); + }, + ), + title: 'Opening and Closing Time', + onConfirm: () { + final updatedState = context.read().state; + if (updatedState is GarageDoorLoadedState) { + context.read().add( + GarageDoorControlEvent( + deviceId: updatedState.status.uuid, + code: 'tr_timecon', + value: updatedState.status.trTimeCon, + ), + ); + Navigator.pop(context); + } + }); + }, + child: PresenceDisplayValue( + value: state.status.trTimeCon.toString(), + postfix: 'sec', + description: 'Opening & Closing Time', + ), + ), + ), + const SizedBox(width: 24), + ], + ) + ], + ), + ); + } + return const SizedBox(); + }, + ), + ); + }, + ); + } +} diff --git a/lib/pages/device_managment/garage_door/models/garage_door_model.dart b/lib/pages/device_managment/garage_door/models/garage_door_model.dart new file mode 100644 index 00000000..60d37d9f --- /dev/null +++ b/lib/pages/device_managment/garage_door/models/garage_door_model.dart @@ -0,0 +1,123 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; + +class GarageDoorStatusModel { + final String uuid; + final bool switch1; + final int countdown1; + final bool doorContactState; + final int trTimeCon; + final int countdownAlarm; + final String doorControl1; + final bool voiceControl1; + final String doorState1; + // final bool isOpen; + final Duration delay; + final List? schedules; // Add schedules field + + GarageDoorStatusModel({ + required this.uuid, + required this.switch1, + required this.countdown1, + required this.doorContactState, + required this.trTimeCon, + required this.countdownAlarm, + required this.doorControl1, + required this.voiceControl1, + required this.doorState1, + // required this.isOpen, + required this.delay, + required this.schedules, // Initialize schedules + }); + + factory GarageDoorStatusModel.fromJson(String id, List jsonList) { + late bool switch1; + late int countdown1; + late bool doorContactState; + late int trTimeCon; + late int countdownAlarm; + late String doorControl1; + late bool voiceControl1; + late String doorState1; + List schedules = []; // Initialize schedules + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + switch1 = status.value ?? false; + break; + case 'countdown_1': + countdown1 = status.value ?? 0; + break; + case 'doorcontact_state': + doorContactState = status.value ?? false; + break; + case 'tr_timecon': + trTimeCon = status.value ?? 0; + break; + case 'countdown_alarm': + countdownAlarm = status.value ?? 0; + break; + case 'door_control_1': + doorControl1 = status.value ?? 'closed'; + break; + case 'voice_control_1': + voiceControl1 = status.value ?? false; + break; + case 'door_state_1': + doorState1 = status.value ?? 'close_time_alarm'; + break; + } + } + + return GarageDoorStatusModel( + uuid: id, + switch1: switch1, + countdown1: countdown1, + doorContactState: doorContactState, + trTimeCon: trTimeCon, + countdownAlarm: countdownAlarm, + doorControl1: doorControl1, + voiceControl1: voiceControl1, + doorState1: doorState1, + // isOpen: doorState1 == 'open' ? true : false, + delay: Duration(seconds: countdown1), + schedules: schedules, // Assign schedules + ); + } + + GarageDoorStatusModel copyWith({ + String? uuid, + bool? switch1, + int? countdown1, + bool? doorContactState, + int? trTimeCon, + int? countdownAlarm, + String? doorControl1, + bool? voiceControl1, + String? doorState1, + // bool? isOpen, + Duration? delay, + List? schedules, // Add schedules to copyWith + }) { + return GarageDoorStatusModel( + uuid: uuid ?? this.uuid, + switch1: switch1 ?? this.switch1, + countdown1: countdown1 ?? this.countdown1, + doorContactState: doorContactState ?? this.doorContactState, + trTimeCon: trTimeCon ?? this.trTimeCon, + countdownAlarm: countdownAlarm ?? this.countdownAlarm, + doorControl1: doorControl1 ?? this.doorControl1, + voiceControl1: voiceControl1 ?? this.voiceControl1, + doorState1: doorState1 ?? this.doorState1, + // isOpen: isOpen ?? this.isOpen, + delay: delay ?? this.delay, + schedules: schedules ?? this.schedules, // Copy schedules + ); + } + + @override + String toString() { + return 'GarageDoorStatusModel(uuid: $uuid, switch1: $switch1, countdown1: $countdown1, doorContactState: $doorContactState, trTimeCon: $trTimeCon, countdownAlarm: $countdownAlarm, doorControl1: $doorControl1, voiceControl1: $voiceControl1, doorState1: $doorState1, delay: $delay, schedules: $schedules)'; + } +} diff --git a/lib/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart b/lib/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart new file mode 100644 index 00000000..8c8b60cf --- /dev/null +++ b/lib/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class GarageDoorBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + final List deviceIds; + + const GarageDoorBatchControlView({Key? key, required this.deviceIds}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GarageDoorBloc(deviceId: deviceIds.first) + ..add(GarageDoorFetchBatchStatusEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is GarageDoorLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is GarageDoorBatchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is GarageDoorBatchControlError) { + return Center(child: Text('Error: ${state.message}')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, GarageDoorStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Garage Door', + icon: status.switch1 ? Assets.openedDoor : Assets.closedDoor, + onChange: (value) { + context.read().add( + GarageDoorBatchControlEvent( + deviceIds: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + GarageDoorFactoryResetEvent( + deviceId: deviceIds.first, + factoryReset: FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart new file mode 100644 index 00000000..886ca9ae --- /dev/null +++ b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class GarageDoorControlView extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + + const GarageDoorControlView({required this.deviceId, super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GarageDoorBloc(deviceId: deviceId)..add(GarageDoorInitialEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is GarageDoorLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is GarageDoorReportsState) { + return ReportsTable( + report: state.deviceReport, + hideValueShowDescription: true, + garageDoorSensor: true, + onRowTap: (index) {}, + onClose: () { + context.read().add(BackToGarageDoorGridViewEvent()); + }, + ); + } else if (state is GarageDoorLoadedState) { + return _buildControlView(context, state.status); + } else if (state is GarageDoorErrorState) { + return Center(child: Text('Error: ${state.message}')); + } + return const Center(child: Text('Unknown state')); + }, + ), + ); + } + + Widget _buildControlView(BuildContext context, GarageDoorStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + + return GridView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 50), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + childAspectRatio: 1.5, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + IconNameStatusContainer( + isFullIcon: false, + name: status.switch1 ? 'Opened' : 'Closed', + icon: status.switch1 ? Assets.openedDoor : Assets.closedDoor, + onTap: () { + context.read().add( + GarageDoorControlEvent(deviceId: status.uuid, value: !status.switch1, code: 'switch_1'), + ); + }, + status: status.switch1, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + onTap: () { + context.read().add( + FetchGarageDoorSchedulesEvent(deviceId: deviceId, category: 'switch_1'), + ); + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildGarageDoorScheduleView(status: status), + )); + }, + name: 'Scheduling', + icon: Assets.acSchedule, + status: false, + textColor: ColorsManager.blackColor, + isFullIcon: false, + ), + ToggleWidget( + label: '', + labelWidget: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + context.read().add(DecreaseGarageDoorDelayEvent(deviceId: status.uuid)); + }, + icon: const Icon( + Icons.remove, + size: 28, + color: ColorsManager.greyColor, + ), + padding: EdgeInsets.zero, + ), + Text( + status.delay.inHours.toString().padLeft(2, '0'), + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'h', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor), + ), + Text( + (status.delay.inMinutes % 60).toString().padLeft(2, '0'), + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'm', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor), + ), + IconButton( + onPressed: () { + context.read().add(IncreaseGarageDoorDelayEvent(deviceId: status.uuid)); + }, + icon: const Icon( + Icons.add, + size: 28, + color: ColorsManager.greyColor, + ), + padding: EdgeInsets.zero, + ), + ], + ), + value: status.countdown1 != 0 ? true : false, + code: 'countdown_1', + deviceId: status.uuid, + icon: Assets.doorDelay, + onChange: (value) { + context.read().add( + GarageDoorControlEvent( + deviceId: status.uuid, value: value ? status.delay.inSeconds : 0, code: 'countdown_1'), + ); + }, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Records', + icon: Assets.records, + onTap: () { + context.read().add(FetchGarageDoorRecordsEvent(code: 'switch_1', deviceId: status.uuid)); + }, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Preferences', + icon: Assets.preferences, + onTap: () { + GarageDoorDialogHelper.showPreferencesDialog(context); + }, + status: false, + textColor: ColorsManager.blackColor, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart b/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart new file mode 100644 index 00000000..843bac9b --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart @@ -0,0 +1,53 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/seconds_picker.dart'; + +class OpeningAndClosingTimeDialogBody extends StatefulWidget { + final ValueChanged onDurationChanged; + final GarageDoorBloc bloc; + + OpeningAndClosingTimeDialogBody({ + required this.onDurationChanged, + required this.bloc, + }); + + @override + _OpeningAndClosingTimeDialogBodyState createState() => + _OpeningAndClosingTimeDialogBodyState(); +} + +class _OpeningAndClosingTimeDialogBodyState + extends State { + late int durationInSeconds; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final currentState = widget.bloc.state; + if (currentState is GarageDoorLoadedState) { + setState(() { + durationInSeconds = currentState.status.trTimeCon; + }); + } + } + + @override + Widget build(BuildContext context) { + return Container( + height: 120, + color: Colors.white, + child: SecondsPicker( + initialSeconds: durationInSeconds, + onSecondsChanged: (newSeconds) { + setState(() { + durationInSeconds = newSeconds; + }); + widget.onDurationChanged(newSeconds); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart b/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart new file mode 100644 index 00000000..07cd9c7a --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; + +class ScheduleGarageTableWidget extends StatelessWidget { + final GarageDoorLoadedState state; + + const ScheduleGarageTableWidget({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Table( + border: TableBorder.all( + color: ColorsManager.graysColor, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), + children: [ + TableRow( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + children: [ + _buildTableHeader('Active'), + _buildTableHeader('Days'), + _buildTableHeader('Time'), + _buildTableHeader('Function'), + _buildTableHeader('Action'), + ], + ), + ], + ), + BlocBuilder( + builder: (context, state) { + if (state is ScheduleGarageLoadingState) { + return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + } + if (state is GarageDoorLoadedState && state.status.schedules?.isEmpty == true) { + return _buildEmptyState(context); + } else if (state is GarageDoorLoadedState) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: + const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + child: _buildTableBody(state, context)); + } + return const SizedBox( + height: 200, + ); + }, + ), + ], + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'No schedules added yet', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTableBody(GarageDoorLoadedState state, BuildContext context) { + return SizedBox( + height: 200, + child: SingleChildScrollView( + child: Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + if (state.status.schedules != null) + for (int i = 0; i < state.status.schedules!.length; i++) + _buildScheduleRow(state.status.schedules![i], i, context, state), + ], + ), + ), + ); + } + + Widget _buildTableHeader(String label) { + return TableCell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + label, + style: const TextStyle( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ); + } + + TableRow _buildScheduleRow(ScheduleModel schedule, int index, BuildContext context, GarageDoorLoadedState state) { + return TableRow( + children: [ + Center( + child: GestureDetector( + onTap: () { + context.read().add(UpdateGarageDoorScheduleEvent( + index: index, + enable: !schedule.enable, + scheduleId: schedule.scheduleId, + deviceId: state.status.uuid, + functionOn: schedule.function.value, + )); + }, + child: SizedBox( + width: 24, + height: 24, + child: schedule.enable + ? const Icon(Icons.radio_button_checked, color: ColorsManager.blueColor) + : const Icon( + Icons.radio_button_unchecked, + color: ColorsManager.grayColor, + ), + ), + ), + ), + Center(child: Text(_getSelectedDays(ScheduleModel.parseSelectedDays(schedule.days)))), + Center(child: Text(formatIsoStringToTime(schedule.time, context))), + Center(child: Text(schedule.function.value ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + GarageDoorDialogHelper.showAddGarageDoorScheduleDialog(context, + schedule: schedule, index: index, isEdit: true); + }, + child: Text( + 'Edit', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor), + ), + ), + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + context.read().add(DeleteGarageDoorScheduleEvent( + index: index, + scheduleId: schedule.scheduleId, + deviceId: state.status.uuid, + )); + }, + child: Text( + 'Delete', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart b/lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart new file mode 100644 index 00000000..cf42e6a3 --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleGarageHeader extends StatelessWidget { + const ScheduleGarageHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: const EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart b/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart new file mode 100644 index 00000000..e5819e89 --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule__garage_table.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleGarageManagementUI extends StatelessWidget { + final GarageDoorLoadedState state; + final Function onAddSchedule; + + const ScheduleGarageManagementUI({ + super.key, + required this.state, + required this.onAddSchedule, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 170, + height: 40, + child: DefaultButton( + borderColor: ColorsManager.boxColor, + padding: 2, + backgroundColor: ColorsManager.graysColor, + borderRadius: 15, + onPressed: () => onAddSchedule(), + child: Row( + children: [ + const Icon(Icons.add, color: ColorsManager.primaryColor), + Text( + ' Add new schedule', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ScheduleGarageTableWidget(state: state), + ], + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart b/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart new file mode 100644 index 00000000..b30c3596 --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleGarageModeButtons extends StatelessWidget { + final VoidCallback onSave; + + const ScheduleGarageModeButtons({ + super.key, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: DefaultButton( + height: 40, + onPressed: onSave, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart b/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart new file mode 100644 index 00000000..7b6e4690 --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleGarageDoorModeSelector extends StatelessWidget { + final GarageDoorLoadedState state; + + const ScheduleGarageDoorModeSelector({super.key, required this.state}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), + ], + ), + ], + ); + } + + Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, GarageDoorLoadedState state) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: state.scheduleMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + if (value == ScheduleModes.schedule) { + context.read().add( + FetchGarageDoorSchedulesEvent( + category: 'switch_1', + deviceId: state.status.uuid, + ), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart b/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart new file mode 100644 index 00000000..107c8e0a --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_header.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart'; + +class BuildGarageDoorScheduleView extends StatefulWidget { + const BuildGarageDoorScheduleView({super.key, required this.status}); + + final GarageDoorStatusModel status; + + @override + State createState() => _BuildScheduleViewState(); +} + +class _BuildScheduleViewState extends State { + @override + Widget build(BuildContext context) { + final bloc = BlocProvider.of(context); + + return BlocProvider.value( + value: bloc, + child: Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 700, + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is GarageDoorLoadedState) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ScheduleGarageHeader(), + const SizedBox(height: 20), + ScheduleGarageManagementUI( + state: state, + onAddSchedule: () { + GarageDoorDialogHelper + .showAddGarageDoorScheduleDialog(context, + schedule: null, index: null, isEdit: false); + }, + ), + const SizedBox(height: 20), + ScheduleGarageModeButtons( + onSave: () { + Navigator.pop(context); + }, + ), + ], + ); + } + if (state is ScheduleGarageLoadingState) { + return SizedBox( + height: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ScheduleGarageHeader(), + const SizedBox( + height: 50, + ), + const Center(child: CircularProgressIndicator()), + const SizedBox( + height: 20, + ), + ScheduleGarageModeButtons( + onSave: () {}, + ), + ], + )); + } + return const SizedBox( + height: 200, + child: ScheduleGarageHeader(), + ); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/seconds_picker.dart b/lib/pages/device_managment/garage_door/widgets/seconds_picker.dart new file mode 100644 index 00000000..491be37b --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/seconds_picker.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class SecondsPicker extends StatefulWidget { + final int initialSeconds; + final ValueChanged onSecondsChanged; + + SecondsPicker({ + required this.initialSeconds, + required this.onSecondsChanged, + }); + + @override + _SecondsPickerState createState() => _SecondsPickerState(); +} + +class _SecondsPickerState extends State { + late FixedExtentScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = FixedExtentScrollController( + initialItem: widget.initialSeconds, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 120, + color: Colors.white, + child: ListWheelScrollView.useDelegate( + controller: _scrollController, + itemExtent: 48, + onSelectedItemChanged: (index) { + widget.onSecondsChanged(index); + }, + physics: const FixedExtentScrollPhysics(), + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + '$index sec', + style: const TextStyle(fontSize: 24), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart b/lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart new file mode 100644 index 00000000..541ab9e4 --- /dev/null +++ b/lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart @@ -0,0 +1,49 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; + +class TimeOutAlarmDialogBody extends StatefulWidget { + TimeOutAlarmDialogBody(this.bloc); + final GarageDoorBloc bloc; + + @override + _TimeOutAlarmDialogBodyState createState() => _TimeOutAlarmDialogBodyState(); +} + +class _TimeOutAlarmDialogBodyState extends State { + int durationInSeconds = 0; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final currentState = widget.bloc.state; + if (currentState is GarageDoorLoadedState) { + if (currentState.status.countdownAlarm != 0) { + setState(() { + durationInSeconds = currentState.status.countdownAlarm; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Container( + height: 120, + color: Colors.white, + child: CupertinoTimerPicker( + itemExtent: 120, + mode: CupertinoTimerPickerMode.hm, + initialTimerDuration: Duration(seconds: durationInSeconds), + onTimerDurationChanged: (newDuration) { + widget.bloc.add( + UpdateCountdownAlarmEvent(newDuration.inSeconds), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart b/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart index ca572bf0..e14672ae 100644 --- a/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart +++ b/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -12,14 +13,13 @@ class GateWayBloc extends Bloc { GateWayBloc() : super(GateWayInitial()) { on((event, emit) {}); on(_getGatWayById); + on(_onFactoryReset); } - FutureOr _getGatWayById( - GatWayById event, Emitter emit) async { + FutureOr _getGatWayById(GatWayById event, Emitter emit) async { emit(GatewayLoadingState()); try { - List devicesList = - await DevicesManagementApi.getDevicesByGatewayId(event.getWayId); + List devicesList = await DevicesManagementApi.getDevicesByGatewayId(event.getWayId); emit(UpdateGatewayState(list: devicesList)); } catch (e) { @@ -27,4 +27,21 @@ class GateWayBloc extends Bloc { return; } } + + FutureOr _onFactoryReset(GateWayFactoryReset event, Emitter emit) async { + emit(GatewayLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const ErrorState(message: 'Failed')); + } else { + add(GatWayById(event.deviceId)); + } + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } } diff --git a/lib/pages/device_managment/gateway/bloc/gate_way_event.dart b/lib/pages/device_managment/gateway/bloc/gate_way_event.dart index 22c81a12..6ee5faf5 100644 --- a/lib/pages/device_managment/gateway/bloc/gate_way_event.dart +++ b/lib/pages/device_managment/gateway/bloc/gate_way_event.dart @@ -18,3 +18,15 @@ class GatWayById extends GateWayEvent { final String getWayId; const GatWayById(this.getWayId); } + +class GateWayFactoryReset extends GateWayEvent { + final String deviceId; + final FactoryResetModel factoryReset; + const GateWayFactoryReset({ + required this.deviceId, + required this.factoryReset, + }); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/gateway/view/gateway_batch_control.dart b/lib/pages/device_managment/gateway/view/gateway_batch_control.dart new file mode 100644 index 00000000..cb85b7d9 --- /dev/null +++ b/lib/pages/device_managment/gateway/view/gateway_batch_control.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/gateway/bloc/gate_way_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class GatewayBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + const GatewayBatchControlView({super.key, required this.gatewayIds}); + + final List gatewayIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GateWayBloc()..add(GatWayById(gatewayIds.first)), + child: BlocBuilder( + builder: (context, state) { + if (state is GatewayLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is UpdateGatewayState) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + height: 140, + child: FirmwareUpdateWidget( + deviceId: gatewayIds.first, version: 2)), + const SizedBox( + width: 12, + ), + SizedBox( + width: 170, + height: 140, + child: FactoryResetWidget( + callFactoryReset: () { + context.read().add( + GateWayFactoryReset( + deviceId: gatewayIds.first, + factoryReset: + FactoryResetModel(devicesUuid: gatewayIds), + ), + ); + }, + ), + ), + ], + ); + } else { + return const Center(child: Text('Error fetching status')); + } + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/gateway/view/gateway_view.dart b/lib/pages/device_managment/gateway/view/gateway_view.dart index 4f14161e..d674e4d8 100644 --- a/lib/pages/device_managment/gateway/view/gateway_view.dart +++ b/lib/pages/device_managment/gateway/view/gateway_view.dart @@ -5,15 +5,17 @@ import 'package:syncrow_web/pages/device_managment/gateway/bloc/gate_way_bloc.da import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class GateWayControls extends StatelessWidget with HelperResponsiveLayout { - const GateWayControls({super.key, required this.gatewayId}); +class GateWayControlsView extends StatelessWidget with HelperResponsiveLayout { + const GateWayControlsView({super.key, required this.gatewayId}); final String gatewayId; @override Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); @@ -24,28 +26,64 @@ class GateWayControls extends StatelessWidget with HelperResponsiveLayout { if (state is GatewayLoadingState) { return const Center(child: CircularProgressIndicator()); } else if (state is UpdateGatewayState) { - return GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 50), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isLarge - ? 3 - : isMedium - ? 2 - : 1, - mainAxisExtent: 140, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - ), - itemCount: state.list.length, - itemBuilder: (context, index) { - final device = state.list[index]; - return _DeviceItem(device: device); - }, + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 50), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Bluetooth Devices:", + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 12), + Text( + "No devices found", + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.blackColor, + ), + ), + const SizedBox(height: 30), + Text( + "ZigBee Devices:", + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: state.list.length, + itemBuilder: (context, index) { + final device = state.list[index]; + return _DeviceItem(device: device); + }, + ), + ], ); } else { - return const Center(child: Text('Error fetching devices')); + return const Center(child: Text('Error fetching status')); } }, ), @@ -66,26 +104,23 @@ class _DeviceItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 60, + ClipOval( + child: Container( height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.whiteColors, + child: SvgPicture.asset( + device.icon ?? 'assets/icons/gateway.svg', + width: 35, + height: 35, + fit: BoxFit.contain, ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(4), - child: ClipOval( - child: SvgPicture.asset( - device.icon, - fit: BoxFit.fill, - ), - ), - ), + )), const Spacer(), Text( device.name ?? 'Unknown Device', - textAlign: TextAlign.center, + textAlign: TextAlign.start, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, diff --git a/lib/pages/device_managment/living_room_switch/bloc/living_room_event.dart b/lib/pages/device_managment/living_room_switch/bloc/living_room_event.dart deleted file mode 100644 index f7b57cde..00000000 --- a/lib/pages/device_managment/living_room_switch/bloc/living_room_event.dart +++ /dev/null @@ -1,29 +0,0 @@ -part of 'living_room_bloc.dart'; - -sealed class LivingRoomEvent extends Equatable { - const LivingRoomEvent(); - - @override - List get props => []; -} - -class LivingRoomFetchDeviceStatus extends LivingRoomEvent { - final String deviceId; - - const LivingRoomFetchDeviceStatus(this.deviceId); - - @override - List get props => [deviceId]; -} - -class LivingRoomControl extends LivingRoomEvent { - final String deviceId; - final String code; - final bool value; - - const LivingRoomControl( - {required this.deviceId, required this.code, required this.value}); - - @override - List get props => [deviceId, code, value]; -} diff --git a/lib/pages/device_managment/living_room_switch/widgets/living_toggle_widget.dart b/lib/pages/device_managment/living_room_switch/widgets/living_toggle_widget.dart deleted file mode 100644 index 701d412b..00000000 --- a/lib/pages/device_managment/living_room_switch/widgets/living_toggle_widget.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; - -class ToggleWidget extends StatelessWidget { - final bool value; - final String code; - final String deviceId; - final String label; - - const ToggleWidget({ - super.key, - required this.value, - required this.code, - required this.deviceId, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ColorsManager.greyColor.withOpacity(0.2), - border: Border.all(color: ColorsManager.boxDivider), - ), - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ClipOval( - child: Container( - color: ColorsManager.whiteColors, - child: SvgPicture.asset( - Assets.lightPulp, - width: 60, - height: 60, - fit: BoxFit.cover, - ), - )), - SizedBox( - height: 20, - width: 35, - child: CupertinoSwitch( - value: value, - activeColor: ColorsManager.dialogBlueTitle, - onChanged: (newValue) { - context.read().add( - LivingRoomControl( - deviceId: deviceId, - code: code, - value: newValue, - ), - ); - }, - ), - ), - ], - ), - const Spacer(), - Text( - label, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_bloc.dart b/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_bloc.dart new file mode 100644 index 00000000..933ce28b --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_bloc.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_event.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_state.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/models/main_door_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class MainDoorSensorBloc + extends Bloc { + MainDoorSensorBloc() : super(MainDoorSensorInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onFetchBatchStatus); + on(_fetchReports); + on(_factoryReset); + } + + late MainDoorSensorStatusModel deviceStatus; + Timer? _timer; + + FutureOr _onFetchDeviceStatus(MainDoorSensorFetchDeviceEvent event, + Emitter emit) async { + emit(MainDoorSensorLoadingState()); + try { + final status = await DevicesManagementApi() + .getDeviceStatus(event.deviceId) + .then((value) => value.status); + + deviceStatus = MainDoorSensorStatusModel.fromJson(event.deviceId, status); + emit(MainDoorSensorDeviceStatusLoaded(deviceStatus)); + } catch (e) { + emit(MainDoorSensorFailedState(error: e.toString())); + } + } + + FutureOr _onControl( + MainDoorSensorControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(MainDoorSensorDeviceStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + ); + } + + Future _runDebounce({ + required String deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + }) async { + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + final response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + + if (!response) { + _revertValueAndEmit(deviceId, code, oldValue, emit); + } + } catch (e) { + if (e is DioException && e.response != null) { + debugPrint('Error response: ${e.response?.data}'); + } + _revertValueAndEmit(deviceId, code, oldValue, emit); + } + }); + } + + void _revertValueAndEmit(String deviceId, String code, bool oldValue, + Emitter emit) { + _updateLocalValue(code, oldValue); + emit(MainDoorSensorDeviceStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + switch (code) { + case 'doorcontact_state': + deviceStatus = deviceStatus.copyWith(doorContactState: value); + break; + default: + break; + } + } + + bool _getValueByCode(String code) { + switch (code) { + case 'doorcontact_state': + return deviceStatus.doorContactState; + default: + return false; + } + } + + // Fetch batch status for multiple devices (if needed) + FutureOr _onFetchBatchStatus(MainDoorSensorFetchBatchEvent event, + Emitter emit) async { + emit(MainDoorSensorLoadingState()); + try { + // final batchStatus = + // await DevicesManagementApi().getBatchDeviceStatus(event.deviceIds); + // Assuming you need to update multiple devices status here + // You might need a list or map of MainDoorSensorStatusModel for batch processing + // emit(MainDoorSensorBatchStatusLoaded(batchStatus)); + } catch (e) { + emit(MainDoorSensorBatchFailedState(error: e.toString())); + } + } + + FutureOr _fetchReports(MainDoorSensorReportsEvent event, + Emitter emit) async { + emit(MainDoorSensorLoadingState()); + try { + final reports = await DevicesManagementApi.getDeviceReportsByDate( + event.deviceId, event.code, event.from, event.to); + emit(MainDoorSensorReportLoaded(reports)); + } catch (e) { + emit(MainDoorSensorFailedState(error: e.toString())); + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } + + FutureOr _factoryReset(MainDoorSensorFactoryReset event, + Emitter emit) async { + emit(MainDoorSensorLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(MainDoorSensorFailedState(error: 'Failed')); + } else { + add(MainDoorSensorFetchDeviceEvent(event.deviceId)); + } + } catch (e) { + emit(MainDoorSensorFailedState(error: e.toString())); + } + } +} diff --git a/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_event.dart b/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_event.dart new file mode 100644 index 00000000..c2864333 --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_event.dart @@ -0,0 +1,73 @@ +import 'package:equatable/equatable.dart'; + +import '../../all_devices/models/factory_reset_model.dart'; + +class MainDoorSensorEvent extends Equatable { + @override + List get props => []; +} + +class MainDoorSensorFetchDeviceEvent extends MainDoorSensorEvent { + final String deviceId; + + MainDoorSensorFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class MainDoorSensorFetchBatchEvent extends MainDoorSensorEvent { + final String deviceId; + + MainDoorSensorFetchBatchEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class MainDoorSensorControl extends MainDoorSensorEvent { + final String deviceId; + final String code; + final bool value; + + MainDoorSensorControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class MainDoorSensorBatchControl extends MainDoorSensorEvent { + final List deviceId; + final String code; + final bool value; + + MainDoorSensorBatchControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class MainDoorSensorReportsEvent extends MainDoorSensorEvent { + final String deviceId; + final String code; + final String from; + final String to; + @override + List get props => [deviceId, code, from, to]; + + MainDoorSensorReportsEvent( + {required this.deviceId, + required this.code, + required this.from, + required this.to}); +} + +class MainDoorSensorFactoryReset extends MainDoorSensorEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + MainDoorSensorFactoryReset( + {required this.deviceId, required this.factoryReset}); +} diff --git a/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_state.dart b/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_state.dart new file mode 100644 index 00000000..d482245b --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/bloc/main_door_sensor_state.dart @@ -0,0 +1,68 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/models/main_door_status_model.dart'; + +class MainDoorSensorState extends Equatable { + @override + List get props => []; +} + +class MainDoorSensorInitial extends MainDoorSensorState {} + +class MainDoorSensorLoadingState extends MainDoorSensorState {} + +class MainDoorSensorDeviceStatusLoaded extends MainDoorSensorState { + final MainDoorSensorStatusModel status; + + MainDoorSensorDeviceStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class MainDoorSensorFailedState extends MainDoorSensorState { + final String error; + + MainDoorSensorFailedState({required this.error}); + + @override + List get props => [error]; +} + +class MainDoorSensorBatchFailedState extends MainDoorSensorState { + final String error; + + MainDoorSensorBatchFailedState({required this.error}); + + @override + List get props => [error]; +} + +class MainDoorSensorBatchStatusLoaded extends MainDoorSensorState { + final List status; + + MainDoorSensorBatchStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class MainDoorSensorReportLoaded extends MainDoorSensorState { + final DeviceReport deviceReport; + + MainDoorSensorReportLoaded(this.deviceReport); + + @override + List get props => [deviceReport]; +} + +class MainDoorSensorReportsLoadingState extends MainDoorSensorState {} + +class MainDoorSensorReportsFailedState extends MainDoorSensorState { + final String error; + + MainDoorSensorReportsFailedState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/main_door_sensor/models/main_door_status_model.dart b/lib/pages/device_managment/main_door_sensor/models/main_door_status_model.dart new file mode 100644 index 00000000..52dda7a3 --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/models/main_door_status_model.dart @@ -0,0 +1,47 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class MainDoorSensorStatusModel { + final String uuid; + final bool doorContactState; + final int batteryPercentage; + + MainDoorSensorStatusModel({ + required this.uuid, + required this.doorContactState, + required this.batteryPercentage, + }); + + factory MainDoorSensorStatusModel.fromJson(String id, List jsonList) { + late bool doorContactState = false; + late int batteryPercentage = 0; + + for (var status in jsonList) { + switch (status.code) { + case 'doorcontact_state': + doorContactState = status.value ?? false; + break; + case 'battery_percentage': + batteryPercentage = status.value ?? 0; + break; + } + } + + return MainDoorSensorStatusModel( + uuid: id, + doorContactState: doorContactState, + batteryPercentage: batteryPercentage, + ); + } + + MainDoorSensorStatusModel copyWith({ + String? uuid, + bool? doorContactState, + int? batteryPercentage, + }) { + return MainDoorSensorStatusModel( + uuid: uuid ?? this.uuid, + doorContactState: doorContactState ?? this.doorContactState, + batteryPercentage: batteryPercentage ?? this.batteryPercentage, + ); + } +} diff --git a/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart b/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart new file mode 100644 index 00000000..ecdb367a --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_event.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_state.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/models/main_door_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/widgets/notification_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class MainDoorSensorControlView extends StatelessWidget with HelperResponsiveLayout { + const MainDoorSensorControlView({super.key, required this.device}); + + final AllDevicesModel device; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => MainDoorSensorBloc()..add(MainDoorSensorFetchDeviceEvent(device.uuid!)), + child: BlocBuilder( + builder: (context, state) { + if (state is MainDoorSensorLoadingState || state is MainDoorSensorReportsLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is MainDoorSensorDeviceStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is MainDoorSensorReportLoaded) { + return ReportsTable( + report: state.deviceReport, + onRowTap: (index) {}, + onClose: () { + context.read().add(MainDoorSensorFetchDeviceEvent(device.uuid!)); + }, + hideValueShowDescription: true, + mainDoorSensor: true, + ); + } else if (state is MainDoorSensorFailedState || state is MainDoorSensorBatchFailedState) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + )); + } + + Widget _buildStatusControls(BuildContext context, MainDoorSensorStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + IconNameStatusContainer( + isFullIcon: true, + name: status.doorContactState ? 'Open' : 'Close', + icon: Assets.openCloseDoor, + onTap: () {}, + status: status.doorContactState, + textColor: status.doorContactState ? ColorsManager.red : ColorsManager.blackColor, + paddingAmount: 8, + ), + IconNameStatusContainer( + isFullIcon: true, + name: 'Open/Close\nRecord', + icon: Assets.openCloseRecords, + onTap: () { + final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; + final to = DateTime.now().millisecondsSinceEpoch; + context.read().add( + MainDoorSensorReportsEvent( + deviceId: device.uuid!, + code: 'doorcontact_state', + from: from.toString(), + to: to.toString(), + ), + ); + }, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Notifications\nSettings', + icon: Assets.mainDoorNotifi, + onTap: () { + showDialog( + context: context, + builder: (context) => const NotificationDialog(), + ); + }, + status: false, + textColor: ColorsManager.blackColor, + paddingAmount: 14, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart b/lib/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart new file mode 100644 index 00000000..0cacc0be --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/main_door_sensor/bloc/main_door_sensor_event.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; + +class MainDoorSensorBatchView extends StatelessWidget { + const MainDoorSensorBatchView({super.key, required this.devicesIds}); + + final List devicesIds; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + height: 140, + child: FirmwareUpdateWidget( + deviceId: devicesIds.first, + version: 12, + ), + ), + const SizedBox( + width: 12, + ), + SizedBox( + width: 170, + height: 140, + child: FactoryResetWidget( + callFactoryReset: () { + BlocProvider.of(context).add( + MainDoorSensorFactoryReset( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/main_door_sensor/widgets/notification_dialog.dart b/lib/pages/device_managment/main_door_sensor/widgets/notification_dialog.dart new file mode 100644 index 00000000..ac66b315 --- /dev/null +++ b/lib/pages/device_managment/main_door_sensor/widgets/notification_dialog.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NotificationDialog extends StatefulWidget { + const NotificationDialog({super.key}); + + @override + State createState() => _NotificationDialogState(); +} + +class _NotificationDialogState extends State { + bool isLowBatteryNotificationEnabled = true; + bool isClosingRemindersEnabled = true; + bool isDoorAlarmEnabled = true; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 660, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Notification Settings', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isLowBatteryNotificationEnabled, + code: 'notification', + deviceId: '', + label: 'Low Battery', + onChange: (v) { + setState(() { + isLowBatteryNotificationEnabled = v; + }); + }, + icon: '-1', + ), + ), + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isClosingRemindersEnabled, + code: 'notification', + deviceId: '', + label: 'Closing\nReminders', + onChange: (v) { + setState(() { + isClosingRemindersEnabled = v; + }); + }, + icon: '-1', + ), + ), + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isDoorAlarmEnabled, + code: 'notification', + deviceId: '', + label: 'Door Alarm', + onChange: (v) { + setState(() { + isDoorAlarmEnabled = v; + }); + }, + icon: '-1', + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart new file mode 100644 index 00000000..eb72eecd --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'one_gang_glass_switch_event.dart'; +part 'one_gang_glass_switch_state.dart'; + +class OneGangGlassSwitchBloc extends Bloc { + OneGangGlassStatusModel deviceStatus; + Timer? _timer; + + OneGangGlassSwitchBloc({required String deviceId}) + : deviceStatus = OneGangGlassStatusModel(uuid: deviceId, switch1: false, countDown: 0), + super(OneGangGlassSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFactoryReset); + } + + Future _onFetchDeviceStatus( + OneGangGlassSwitchFetchDeviceEvent event, Emitter emit) async { + emit(OneGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status); + emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(OneGangGlassSwitchError(e.toString())); + } + } + + Future _onControl(OneGangGlassSwitchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _onFactoryReset(OneGangGlassFactoryResetEvent event, Emitter emit) async { + emit(OneGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset(event.factoryReset, event.deviceId); + if (!response) { + emit(OneGangGlassSwitchError('Failed to reset device')); + } else { + emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(OneGangGlassSwitchError(e.toString())); + } + } + + Future _onBatchControl(OneGangGlassSwitchBatchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + Future _onFetchBatchStatus( + OneGangGlassSwitchFetchBatchStatusEvent event, Emitter emit) async { + emit(OneGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); + emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(OneGangGlassSwitchError(e.toString())); + } + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi().deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi().deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _revertValueAndEmit(String deviceId, String code, bool oldValue, Emitter emit) { + _updateLocalValue(code, oldValue); + emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + } + + bool _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.switch1; + default: + return false; + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_event.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_event.dart new file mode 100644 index 00000000..83d9b7b9 --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_event.dart @@ -0,0 +1,50 @@ +part of 'one_gang_glass_switch_bloc.dart'; + +@immutable +abstract class OneGangGlassSwitchEvent {} + +class OneGangGlassSwitchFetchDeviceEvent extends OneGangGlassSwitchEvent { + final String deviceId; + + OneGangGlassSwitchFetchDeviceEvent(this.deviceId); +} + +class OneGangGlassSwitchControl extends OneGangGlassSwitchEvent { + final String deviceId; + final String code; + final bool value; + + OneGangGlassSwitchControl({ + required this.deviceId, + required this.code, + required this.value, + }); +} + +class OneGangGlassSwitchBatchControl extends OneGangGlassSwitchEvent { + final List deviceIds; + final String code; + final bool value; + + OneGangGlassSwitchBatchControl({ + required this.deviceIds, + required this.code, + required this.value, + }); +} + +class OneGangGlassSwitchFetchBatchStatusEvent extends OneGangGlassSwitchEvent { + final List deviceIds; + + OneGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); +} + +class OneGangGlassFactoryResetEvent extends OneGangGlassSwitchEvent { + final FactoryResetModel factoryReset; + final String deviceId; + + OneGangGlassFactoryResetEvent({ + required this.factoryReset, + required this.deviceId, + }); +} diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_state.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_state.dart new file mode 100644 index 00000000..6059d5eb --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_state.dart @@ -0,0 +1,32 @@ +part of 'one_gang_glass_switch_bloc.dart'; + +@immutable +abstract class OneGangGlassSwitchState {} + +class OneGangGlassSwitchInitial extends OneGangGlassSwitchState {} + +class OneGangGlassSwitchLoading extends OneGangGlassSwitchState {} + +class OneGangGlassSwitchStatusLoaded extends OneGangGlassSwitchState { + final OneGangGlassStatusModel status; + + OneGangGlassSwitchStatusLoaded(this.status); +} + +class OneGangGlassSwitchError extends OneGangGlassSwitchState { + final String message; + + OneGangGlassSwitchError(this.message); +} + +class OneGangGlassSwitchBatchStatusLoaded extends OneGangGlassSwitchState { + final OneGangGlassStatusModel status; + + OneGangGlassSwitchBatchStatusLoaded(this.status); +} + +class OneGangGlassSwitchBatchControlError extends OneGangGlassSwitchState { + final String message; + + OneGangGlassSwitchBatchControlError(this.message); +} diff --git a/lib/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart b/lib/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart new file mode 100644 index 00000000..39c96dd0 --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart @@ -0,0 +1,50 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class OneGangGlassStatusModel { + final String uuid; + final bool switch1; + final int countDown; + + OneGangGlassStatusModel({ + required this.uuid, + required this.switch1, + required this.countDown, + }); + + factory OneGangGlassStatusModel.fromJson(String id, List jsonList) { + late bool switch1; + late int countDown; + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + switch1 = status.value ?? false; + break; + case 'countdown_1': + countDown = status.value ?? 0; + break; + } + } + + return OneGangGlassStatusModel( + uuid: id, + switch1: switch1, + countDown: countDown, + ); + } + + OneGangGlassStatusModel copyWith({ + String? uuid, + bool? switch1, + int? countDown, + }) { + return OneGangGlassStatusModel( + uuid: uuid ?? this.uuid, + switch1: switch1 ?? this.switch1, + countDown: countDown ?? this.countDown, + ); + } + + @override + String toString() => 'OneGangGlassStatusModel(uuid: $uuid, switch1: $switch1, countDown: $countDown)'; +} diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart new file mode 100644 index 00000000..4239b08e --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class OneGangGlassSwitchBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + final List deviceIds; + + const OneGangGlassSwitchBatchControlView( + {required this.deviceIds, super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => OneGangGlassSwitchBloc(deviceId: deviceIds.first) + ..add(OneGangGlassSwitchFetchBatchStatusEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is OneGangGlassSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is OneGangGlassSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is OneGangGlassSwitchError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, OneGangGlassStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Wall Light', + onChange: (value) { + context.read().add( + OneGangGlassSwitchBatchControl( + deviceIds: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + OneGangGlassFactoryResetEvent( + factoryReset: FactoryResetModel(devicesUuid: deviceIds), + deviceId: deviceIds.first, + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart new file mode 100644 index 00000000..8914b786 --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + + const OneGangGlassSwitchControlView({required this.deviceId, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + OneGangGlassSwitchBloc(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is OneGangGlassSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is OneGangGlassSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is OneGangGlassSwitchError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, OneGangGlassStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: "Wall Light", + onChange: (value) { + context.read().add( + OneGangGlassSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Preferences', + icon: Assets.preferences, + onChange: (value) {}, + showToggle: false, + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Scheduling', + icon: Assets.scheduling, + onChange: (value) {}, + showToggle: false, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart new file mode 100644 index 00000000..595e7e06 --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart @@ -0,0 +1,175 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class WallLightSwitchBloc + extends Bloc { + WallLightSwitchBloc({required this.deviceId}) + : super(WallLightSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onFetchBatchStatus); + on(_onBatchControl); + on(_onFactoryReset); + } + + late WallLightStatusModel deviceStatus; + final String deviceId; + Timer? _timer; + + FutureOr _onFetchDeviceStatus(WallLightSwitchFetchDeviceEvent event, + Emitter emit) async { + emit(WallLightSwitchLoading()); + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + + deviceStatus = + WallLightStatusModel.fromJson(event.deviceId, status.status); + emit(WallLightSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(WallLightSwitchError(e.toString())); + } + } + + FutureOr _onControl( + WallLightSwitchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(WallLightSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _revertValueAndEmit(String deviceId, String code, bool oldValue, + Emitter emit) { + _updateLocalValue(code, oldValue); + emit(WallLightSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + } + + bool _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.switch1; + default: + return false; + } + } + + Future _onFetchBatchStatus(WallLightSwitchFetchBatchEvent event, + Emitter emit) async { + emit(WallLightSwitchLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + WallLightStatusModel.fromJson(event.devicesIds.first, status.status); + emit(WallLightSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(WallLightSwitchError(e.toString())); + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } + + FutureOr _onBatchControl(WallLightSwitchBatchControl event, + Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(WallLightSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.devicesIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + FutureOr _onFactoryReset( + WallLightFactoryReset event, Emitter emit) async { + emit(WallLightSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(WallLightSwitchError('Failed')); + } else { + emit(WallLightSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(WallLightSwitchError(e.toString())); + } + } +} diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart new file mode 100644 index 00000000..5c601484 --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +class WallLightSwitchEvent extends Equatable { + @override + List get props => []; +} + +class WallLightSwitchFetchDeviceEvent extends WallLightSwitchEvent { + final String deviceId; + + WallLightSwitchFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class WallLightSwitchControl extends WallLightSwitchEvent { + final String deviceId; + final String code; + final bool value; + + WallLightSwitchControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class WallLightSwitchFetchBatchEvent extends WallLightSwitchEvent { + final List devicesIds; + + WallLightSwitchFetchBatchEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class WallLightSwitchBatchControl extends WallLightSwitchEvent { + final List devicesIds; + final String code; + final bool value; + + WallLightSwitchBatchControl( + {required this.devicesIds, required this.code, required this.value}); + + @override + List get props => [devicesIds, code, value]; +} + +class WallLightFactoryReset extends WallLightSwitchEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + WallLightFactoryReset({required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart new file mode 100644 index 00000000..69cdf05e --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; + +class WallLightSwitchState extends Equatable { + @override + List get props => []; +} + +class WallLightSwitchInitial extends WallLightSwitchState {} + +class WallLightSwitchLoading extends WallLightSwitchState {} + +class WallLightSwitchStatusLoaded extends WallLightSwitchState { + final WallLightStatusModel status; + + WallLightSwitchStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class WallLightSwitchError extends WallLightSwitchState { + final String message; + + WallLightSwitchError(this.message); + + @override + List get props => [message]; +} + +class WallLightSwitchControlError extends WallLightSwitchState { + final String message; + + WallLightSwitchControlError(this.message); + + @override + List get props => [message]; +} + +class WallLightSwitchBatchControlError extends WallLightSwitchState { + final String message; + + WallLightSwitchBatchControlError(this.message); + + @override + List get props => [message]; +} + +class WallLightSwitchBatchStatusLoaded extends WallLightSwitchState { + final List status; + + WallLightSwitchBatchStatusLoaded(this.status); + + @override + List get props => [status]; +} diff --git a/lib/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart b/lib/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart new file mode 100644 index 00000000..b479c71d --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart @@ -0,0 +1,47 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class WallLightStatusModel { + final String uuid; + final bool switch1; + final int countDown; + + WallLightStatusModel({ + required this.uuid, + required this.switch1, + required this.countDown, + }); + + factory WallLightStatusModel.fromJson(String id, List jsonList) { + late bool switch1; + late int countDown; + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + switch1 = status.value ?? false; + break; + case 'countdown_1': + countDown = status.value ?? 0; + break; + } + } + + return WallLightStatusModel( + uuid: id, + switch1: switch1, + countDown: countDown, + ); + } + + WallLightStatusModel copyWith({ + String? uuid, + bool? switch1, + int? countDown, + }) { + return WallLightStatusModel( + uuid: uuid ?? this.uuid, + switch1: switch1 ?? this.switch1, + countDown: countDown ?? this.countDown, + ); + } +} diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart new file mode 100644 index 00000000..e1dabb61 --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WallLightBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + const WallLightBatchControlView({super.key, required this.deviceIds}); + + final List deviceIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WallLightSwitchBloc(deviceId: deviceIds.first) + ..add(WallLightSwitchFetchBatchEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is WallLightSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WallLightSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is WallLightSwitchError || + state is WallLightSwitchControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, WallLightStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return SizedBox( + child: GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Wall Light', + onChange: (value) { + context.read().add( + WallLightSwitchBatchControl( + devicesIds: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget( + callFactoryReset: () { + context.read().add(WallLightFactoryReset( + deviceId: status.uuid, + factoryReset: FactoryResetModel(devicesUuid: deviceIds))); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart new file mode 100644 index 00000000..a9e6ebbb --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WallLightDeviceControl extends StatelessWidget + with HelperResponsiveLayout { + final String deviceId; + + const WallLightDeviceControl({super.key, required this.deviceId}); + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WallLightSwitchBloc(deviceId: deviceId) + ..add(WallLightSwitchFetchDeviceEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is WallLightSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WallLightSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is WallLightSwitchError || + state is WallLightSwitchControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, WallLightStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + const SizedBox(), + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: 'Wall Light', + onChange: (value) { + context.read().add(WallLightSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, + )); + }, + ), + const SizedBox(), + ], + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/bloc/smart_power_bloc.dart b/lib/pages/device_managment/power_clamp/bloc/smart_power_bloc.dart new file mode 100644 index 00000000..52cccff4 --- /dev/null +++ b/lib/pages/device_managment/power_clamp/bloc/smart_power_bloc.dart @@ -0,0 +1,792 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_event.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_state.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/models/device_event.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_chart.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class SmartPowerBloc extends Bloc { + SmartPowerBloc({required this.deviceId}) : super(SmartPowerInitial()) { + on(_onFetchDeviceStatus); + on(_onArrowPressed); + on(_onFetchBatchStatus); + on(_onPageChanged); + on(_onBatchControl); + on(_filterRecordsByDate); + on(checkDayMonthYearSelected); + on(_onFactoryReset); + } + + late PowerClampModel deviceStatus; + late PowerClampBatchModel deviceBatchStatus; + final String deviceId; + Timer? _timer; + List> phaseData = []; + int currentPage = 0; + + List record = [ + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:43'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:35'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:29'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:25'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:21'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:17'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:15:07'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:14:47'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:14:40'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:14:23'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2024-10-23 11:14:13'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:43'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:35'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:29'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:25'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:21'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:17'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:15:07'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:14:47'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:14:40'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:14:23'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-10-23 11:14:13'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:43'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:35'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:29'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:25'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:21'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:17'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:15:07'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:14:47'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:14:40'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:14:23'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-23 11:14:13'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-11 11:15:43'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-11 11:15:35'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-12 11:15:29'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-13 11:15:25'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-14 11:15:21'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-15 11:15:17'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-16 11:15:07'), + value: '2286'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-17 11:14:47'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-18 11:14:40'), + value: '2284'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-19 11:14:23'), + value: '2285'), + EventDevice( + code: 'VoltageA', + eventTime: DateTime.parse('2023-02-20 11:14:13'), + value: '2284'), + ]; + + FutureOr _onFetchDeviceStatus( + SmartPowerFetchDeviceEvent event, Emitter emit) async { + emit(SmartPowerLoading()); + try { + var status = + await DevicesManagementApi().getPowerClampInfo(event.deviceId); + deviceStatus = PowerClampModel.fromJson(status); + + phaseData = [ + { + 'name': 'Phase A', + 'voltage': '${deviceStatus.status.phaseA.dataPoints[0].value / 10} V', + 'current': '${deviceStatus.status.phaseA.dataPoints[1].value / 10} A', + 'activePower': '${deviceStatus.status.phaseA.dataPoints[2].value} W', + 'powerFactor': '${deviceStatus.status.phaseA.dataPoints[3].value}', + }, + { + 'name': 'Phase B', + 'voltage': '${deviceStatus.status.phaseB.dataPoints[0].value / 10} V', + 'current': '${deviceStatus.status.phaseB.dataPoints[1].value / 10} A', + 'activePower': '${deviceStatus.status.phaseB.dataPoints[2].value} W', + 'powerFactor': '${deviceStatus.status.phaseB.dataPoints[3].value}', + }, + { + 'name': 'Phase C', + 'voltage': '${deviceStatus.status.phaseC.dataPoints[0].value / 10} V', + 'current': '${deviceStatus.status.phaseC.dataPoints[1].value / 10} A', + 'activePower': '${deviceStatus.status.phaseC.dataPoints[2].value} W', + 'powerFactor': '${deviceStatus.status.phaseC.dataPoints[3].value}', + }, + ]; + emit(GetDeviceStatus()); + } catch (e) { + emit(SmartPowerError(e.toString())); + } + } + + FutureOr _onArrowPressed( + SmartPowerArrowPressedEvent event, Emitter emit) { + currentPage = (currentPage + event.direction + 4) % 4; + emit(SmartPowerStatusLoaded(deviceStatus, currentPage)); + emit(GetDeviceStatus()); + } + + FutureOr _onPageChanged( + SmartPowerPageChangedEvent event, Emitter emit) { + currentPage = event.page; + emit(SmartPowerStatusLoaded(deviceStatus, currentPage)); + emit(GetDeviceStatus()); + } + + Future _onFactoryReset( + SmartPowerFactoryReset event, Emitter emit) async { + emit(SmartPowerLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (response) { + emit(SmartPowerInitial()); + } else { + emit(SmartPowerError('Factory reset failed')); + } + } catch (e) { + emit(SmartPowerError(e.toString())); + } + } + + Future _onBatchControl( + PowerBatchControlEvent event, Emitter emit) async { + final oldValue = deviceStatus.status; + + _updateLocalValue(event.code, event.value); + // emit(WaterLeakBatchStatusLoadedState(deviceStatus!)); + + await _runDebounce( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + Future _onFetchBatchStatus( + SmartPowerFetchBatchEvent event, Emitter emit) async { + emit(SmartPowerLoading()); + try { + final response = + await DevicesManagementApi().getPowerStatus(event.devicesIds); + PowerClampBatchModel deviceStatus = + PowerClampBatchModel.fromJson(response); + + emit(SmartPowerLoadBatchControll(deviceStatus)); + } catch (e) { + debugPrint('=========error====$e'); + emit(SmartPowerError(e.toString())); + } + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required dynamic value, + required dynamic oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _updateLocalValue(String code, dynamic value) { + if (code == 'watersensor_state') { + deviceStatus = deviceStatus.copyWith(statusPower: value); + } else if (code == 'battery_percentage') { + deviceStatus = deviceStatus.copyWith(statusPower: value); + } + } + + void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, + Emitter emit) { + _updateLocalValue(code, oldValue); + emit(SmartPowerLoadBatchControll(deviceBatchStatus)); + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } + + List filteredRecords = []; + + int currentIndex = 0; + final List views = ['Day', 'Month', 'Year']; + + Widget dateSwitcher() { + void switchView(int direction) { + currentIndex = (currentIndex + direction + views.length) % views.length; + } + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.arrow_left), + onPressed: () { + setState(() { + switchView(-1); + }); + }, + ), + Text( + views[currentIndex], + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + ), + IconButton( + icon: const Icon(Icons.arrow_right), + onPressed: () { + setState(() { + switchView(1); + }); + }, + ), + ], + ); + }, + ); + } + + Future selectMonthAndYear(BuildContext context) async { + int selectedYear = DateTime.now().year; + int selectedMonth = DateTime.now().month; + + FixedExtentScrollController yearController = + FixedExtentScrollController(initialItem: selectedYear - 1905); + FixedExtentScrollController monthController = + FixedExtentScrollController(initialItem: selectedMonth - 1); + + return await showDialog( + context: context, + builder: (BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + height: 350, + width: 350, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Select Month and Year', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + const Divider(), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + Expanded( + child: ListWheelScrollView.useDelegate( + controller: yearController, + overAndUnderCenterOpacity: 0.2, + itemExtent: 50, + onSelectedItemChanged: (index) { + selectedYear = 1905 + index; + }, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + (1905 + index).toString(), + style: const TextStyle(fontSize: 18), + ), + ); + }, + childCount: 200, + ), + ), + ), + Expanded( + flex: 2, + child: ListWheelScrollView.useDelegate( + controller: monthController, + overAndUnderCenterOpacity: 0.2, + itemExtent: 50, + onSelectedItemChanged: (index) { + selectedMonth = index + 1; + }, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + DateFormat.MMMM() + .format(DateTime(0, index + 1)), + style: const TextStyle(fontSize: 18), + ), + ); + }, + childCount: 12, + ), + ), + ), + const Spacer(), + ], + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('OK'), + onPressed: () { + final selectedDateTime = + DateTime(selectedYear, selectedMonth); + Navigator.of(context).pop(selectedDateTime); + }, + ), + ], + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Future selectYear(BuildContext context) async { + int selectedYear = DateTime.now().year; + FixedExtentScrollController yearController = + FixedExtentScrollController(initialItem: selectedYear - 1905); + + return await showDialog( + context: context, + builder: (BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + height: 350, + width: 350, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Select Year', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + const Divider(), + Expanded( + child: ListWheelScrollView.useDelegate( + controller: yearController, + overAndUnderCenterOpacity: 0.2, + itemExtent: 50, + onSelectedItemChanged: (index) { + selectedYear = 1905 + index; + }, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + (1905 + index).toString(), + style: const TextStyle(fontSize: 18), + ), + ); + }, + childCount: 200, + ), + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('OK'), + onPressed: () { + final selectedDateTime = DateTime(selectedYear); + Navigator.of(context).pop(selectedDateTime); + }, + ), + ], + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Future dayMonthYearPicker({ + required BuildContext context, + }) async { + DateTime selectedDate = DateTime.now(); + + return await showDialog( + context: context, + builder: (BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 350, + width: 350, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + children: [ + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: DateTime.now(), + minimumYear: 1900, + maximumYear: DateTime.now().year, + onDateTimeChanged: (DateTime newDateTime) { + selectedDate = newDateTime; + }, + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(selectedDate); + }, + ), + ], + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + DateTime? dateTime = DateTime.now(); + + String formattedDate = DateFormat('yyyy/MM/dd').format(DateTime.now()); + + void checkDayMonthYearSelected( + SelectDateEvent event, Emitter emit) async { + Future Function(BuildContext context)? dateSelector; + String dateFormat; + switch (currentIndex) { + case 0: + dateSelector = (context) { + return dayMonthYearPicker(context: context); + }; + dateFormat = 'yyyy/MM/dd'; + break; + case 1: + dateSelector = (context) { + return selectMonthAndYear(context); + }; + dateFormat = 'yyyy-MM'; + break; + case 2: + dateSelector = (context) { + return selectYear(context); + }; + dateFormat = 'yyyy'; + break; + default: + return; + } + Future.delayed(const Duration(milliseconds: 500), () { + emit(FakeState()); + }); + // Use the selected picker + await dateSelector(event.context).then((newDate) { + if (newDate.toString() == 'null') { + emit(GetDeviceStatus()); + } else { + dateTime = newDate; + add(FilterRecordsByDateEvent( + selectedDate: newDate!, + viewType: views[currentIndex], + )); + } + // formattedDate = newDate.toString(); + }); + emit(FilterRecordsState(filteredRecords: energyDataList)); + } + + List energyDataList = []; + void _filterRecordsByDate( + FilterRecordsByDateEvent event, Emitter emit) { + // emit(SmartPowerLoading()); + + if (event.viewType == 'Year') { + formattedDate = event.selectedDate.year.toString(); + filteredRecords = record + .where((record) => record.eventTime!.year == event.selectedDate.year) + .toList(); + } else if (event.viewType == 'Month') { + formattedDate = + "${event.selectedDate.year.toString()}-${getMonthShortName(event.selectedDate.month)}"; + + filteredRecords = record + .where((record) => + record.eventTime!.year == event.selectedDate.year && + record.eventTime!.month == event.selectedDate.month) + .toList(); + } else if (event.viewType == 'Day') { + formattedDate = + "${event.selectedDate.year.toString()}-${getMonthShortName(event.selectedDate.month)}-${event.selectedDate.day}"; + + filteredRecords = record + .where((record) => + record.eventTime!.year == event.selectedDate.year && + record.eventTime!.month == event.selectedDate.month && + record.eventTime!.day == event.selectedDate.day) + .toList(); + } + + selectDateRange(); + energyDataList = filteredRecords.map((eventDevice) { + return EnergyData( + event.viewType == 'Year' + ? getMonthShortName( + int.tryParse(DateFormat('MM').format(eventDevice.eventTime!))!) + : event.viewType == 'Month' + ? DateFormat('yyyy/MM/dd').format(eventDevice.eventTime!) + : DateFormat('HH:mm:ss').format(eventDevice.eventTime!), + double.parse(eventDevice.value!), + ); + }).toList(); + emit(FilterRecordsState(filteredRecords: energyDataList)); + } + + String getMonthShortName(int month) { + final date = DateTime(0, month); + return DateFormat.MMM().format(date); + } + + String endChartDate = ''; + + void selectDateRange() async { + DateTime startDate = dateTime!; + DateTime endDate = DateTime(startDate.year, startDate.month + 1, 1) + .subtract(Duration(days: 1)); + String formattedEndDate = DateFormat('dd/MM/yyyy').format(endDate); + endChartDate = ' - $formattedEndDate'; + } +} diff --git a/lib/pages/device_managment/power_clamp/bloc/smart_power_event.dart b/lib/pages/device_managment/power_clamp/bloc/smart_power_event.dart new file mode 100644 index 00000000..1985c67c --- /dev/null +++ b/lib/pages/device_managment/power_clamp/bloc/smart_power_event.dart @@ -0,0 +1,115 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +class SmartPowerEvent extends Equatable { + @override + List get props => []; +} + +class SmartPowerFetchDeviceEvent extends SmartPowerEvent { + final String deviceId; + + SmartPowerFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class SmartPowerControl extends SmartPowerEvent { + final String deviceId; + final String code; + final bool value; + + SmartPowerControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class SmartPowerFetchBatchEvent extends SmartPowerEvent { + final List devicesIds; + + SmartPowerFetchBatchEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class SmartPowerBatchControl extends SmartPowerEvent { + final List devicesIds; + final String code; + final bool value; + + SmartPowerBatchControl( + {required this.devicesIds, required this.code, required this.value}); + + @override + List get props => [devicesIds, code, value]; +} + +class SmartPowerFactoryReset extends SmartPowerEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + SmartPowerFactoryReset({required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} + +class PageChangedEvent extends SmartPowerEvent { + final int newPage; + PageChangedEvent(this.newPage); +} + +class PageArrowPressedEvent extends SmartPowerEvent { + final int direction; + PageArrowPressedEvent(this.direction); +} + +class SmartPowerArrowPressedEvent extends SmartPowerEvent { + final int direction; + SmartPowerArrowPressedEvent(this.direction); +} + +class SmartPowerPageChangedEvent extends SmartPowerEvent { + final int page; + SmartPowerPageChangedEvent(this.page); +} + +class SelectDateEvent extends SmartPowerEvent { + BuildContext context; + SelectDateEvent({required this.context}); +} + +class FilterRecordsByDateEvent extends SmartPowerEvent { + final DateTime selectedDate; + final String viewType; // 'Day', 'Month', 'Year' + + FilterRecordsByDateEvent( + {required this.selectedDate, required this.viewType}); +} + +class FetchPowerClampBatchStatusEvent extends SmartPowerEvent { + final List deviceIds; + + FetchPowerClampBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; +}class PowerBatchControlEvent extends SmartPowerEvent { + final List deviceIds; + final String code; + final dynamic value; + + PowerBatchControlEvent({ + required this.deviceIds, + required this.code, + required this.value, + }); + + @override + List get props => [deviceIds, code, value]; +} \ No newline at end of file diff --git a/lib/pages/device_managment/power_clamp/bloc/smart_power_state.dart b/lib/pages/device_managment/power_clamp/bloc/smart_power_state.dart new file mode 100644 index 00000000..a46a3223 --- /dev/null +++ b/lib/pages/device_managment/power_clamp/bloc/smart_power_state.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_chart.dart'; + +class SmartPowerState extends Equatable { + @override + List get props => []; +} + +class SmartPowerInitial extends SmartPowerState {} + +class SmartPowerLoading extends SmartPowerState {} + +class GetDeviceStatus extends SmartPowerState {} +//GetDeviceStatus + +class SmartPowerLoadBatchControll extends SmartPowerState { + final PowerClampBatchModel status; + + SmartPowerLoadBatchControll(this.status); + + @override + List get props => [status]; +} + +class DateSelectedState extends SmartPowerState {} + +class FakeState extends SmartPowerState {} + +class SmartPowerStatusLoaded extends SmartPowerState { + final PowerClampModel deviceStatus; + final int currentPage; + SmartPowerStatusLoaded(this.deviceStatus, this.currentPage); +} + +class SmartPowerError extends SmartPowerState { + final String message; + + SmartPowerError(this.message); + + @override + List get props => [message]; +} + +class SmartPowerControlError extends SmartPowerState { + final String message; + + SmartPowerControlError(this.message); + + @override + List get props => [message]; +} + +class SmartPowerBatchControlError extends SmartPowerState { + final String message; + + SmartPowerBatchControlError(this.message); + + @override + List get props => [message]; +} + +class SmartPowerBatchStatusLoaded extends SmartPowerState { + final List status; + + SmartPowerBatchStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class FilterRecordsState extends SmartPowerState { + final List filteredRecords; + + FilterRecordsState({required this.filteredRecords}); +} diff --git a/lib/pages/device_managment/power_clamp/models/device_event.dart b/lib/pages/device_managment/power_clamp/models/device_event.dart new file mode 100644 index 00000000..09f7b46e --- /dev/null +++ b/lib/pages/device_managment/power_clamp/models/device_event.dart @@ -0,0 +1,23 @@ + +class EventDevice { + final String? code; + final DateTime? eventTime; + final String? value; + + EventDevice({ + this.code, + this.eventTime, + this.value, + }); + + EventDevice.fromJson(Map json) + : code = json['code'] as String?, + eventTime = json['eventTime'] , + value = json['value'] as String?; + + Map toJson() => { + 'code': code, + 'eventTime': eventTime, + 'value': value, + }; +} diff --git a/lib/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart b/lib/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart new file mode 100644 index 00000000..1812d1c9 --- /dev/null +++ b/lib/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart @@ -0,0 +1,49 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +abstract class PowerClampModel1 { + String get productUuid; + String get productType; +} + +class PowerClampBatchModel extends PowerClampModel1 { + @override + final String productUuid; + @override + final String productType; + final List status; + + PowerClampBatchModel({ + required this.productUuid, + required this.productType, + required this.status, + }); + + factory PowerClampBatchModel.fromJson(Map json) { + String productUuid = json['productUuid'] ?? ''; + String productType = json['productType'] ?? ''; + + List statusList = []; + if (json['status'] != null && json['status'] is List) { + statusList = + (json['status'] as List).map((e) => Status.fromJson(e)).toList(); + } + + return PowerClampBatchModel( + productUuid: productUuid, + productType: productType, + status: statusList, + ); + } + + PowerClampBatchModel copyWith({ + String? productUuid, + String? productType, + List? status, + }) { + return PowerClampBatchModel( + productUuid: productUuid ?? this.productUuid, + productType: productType ?? this.productType, + status: status ?? this.status, + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart b/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart new file mode 100644 index 00000000..914a255b --- /dev/null +++ b/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart @@ -0,0 +1,98 @@ +// PowerClampModel class to represent the response +class PowerClampModel { + String productUuid; + String productType; + PowerStatus status; + + PowerClampModel({ + required this.productUuid, + required this.productType, + required this.status, + }); + + factory PowerClampModel.fromJson(Map json) { + return PowerClampModel( + productUuid: json['productUuid'], + productType: json['productType'], + status: PowerStatus.fromJson(json['status']), + ); + } + + PowerClampModel copyWith({ + String? productUuid, + String? productType, + PowerStatus? statusPower, + }) { + return PowerClampModel( + productUuid: productUuid ?? this.productUuid, + productType: productType ?? this.productType, + status: statusPower ?? this.status, + ); + } +} + +class PowerStatus { + Phase phaseA; + Phase phaseB; + Phase phaseC; + Phase general; + + PowerStatus({ + required this.phaseA, + required this.phaseB, + required this.phaseC, + required this.general, + }); + + factory PowerStatus.fromJson(Map json) { + return PowerStatus( + phaseA: Phase.fromJson(json['phaseA']), + phaseB: Phase.fromJson(json['phaseB']), + phaseC: Phase.fromJson(json['phaseC']), + general: Phase.fromJson(json['general'] + // List.from( + // json['general'].map((x) => DataPoint.fromJson(x))), + )); + } +} + +class Phase { + List dataPoints; + + Phase({required this.dataPoints}); + + factory Phase.fromJson(List json) { + return Phase( + dataPoints: json.map((x) => DataPoint.fromJson(x)).toList(), + ); + } +} + +class DataPoint { + dynamic code; + dynamic customName; + dynamic dpId; + dynamic time; + dynamic type; + dynamic value; + + DataPoint({ + required this.code, + required this.customName, + required this.dpId, + required this.time, + required this.type, + required this.value, + }); + + factory DataPoint.fromJson(Map json) { + return DataPoint( + code: json['code'], + customName: json['customName'], + dpId: json['dpId'], + time: json['time'], + type: json['type'], + value: json['value'], + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/view/phase_widget.dart b/lib/pages/device_managment/power_clamp/view/phase_widget.dart new file mode 100644 index 00000000..223acd95 --- /dev/null +++ b/lib/pages/device_managment/power_clamp/view/phase_widget.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_info_card.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class PhaseWidget extends StatefulWidget { + final List> phaseData; + + PhaseWidget({ + required this.phaseData, + }); + @override + _PhaseWidgetState createState() => _PhaseWidgetState(); +} + +class _PhaseWidgetState extends State { + int _selectedPhaseIndex = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: 10), + Row( + children: List.generate(widget.phaseData.length, (index) { + return InkWell( + onTap: () { + setState(() { + _selectedPhaseIndex = index; + }); + }, + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Text( + widget.phaseData[index]['name'], + style: TextStyle( + fontWeight: FontWeight.bold, + color: _selectedPhaseIndex == index + ? Colors.black + : Colors.grey, + ), + ), + ), + ); + }), + ), + SizedBox(height: 10), + _selectedPhaseIndex == 0 + ? phase( + totalActive: widget.phaseData[0]['activePower'] ?? '0', + totalCurrent: widget.phaseData[0]['current'] ?? '0', + totalFactor: widget.phaseData[0]['powerFactor'] ?? '0', + totalVoltage: widget.phaseData[0]['voltage'] ?? '0', + ) + : _selectedPhaseIndex == 1 + ? phase( + totalActive: widget.phaseData[1]['activePower'] ?? '0', + totalCurrent: widget.phaseData[1]['current'] ?? '0', + totalFactor: widget.phaseData[1]['powerFactor'] ?? '0', + totalVoltage: widget.phaseData[1]['voltage'] ?? '0', + ) + : phase( + totalActive: widget.phaseData[2]['activePower'] ?? '0', + totalCurrent: widget.phaseData[2]['current'] ?? '0', + totalFactor: widget.phaseData[2]['powerFactor'] ?? '0', + totalVoltage: widget.phaseData[2]['voltage'] ?? '0', + ), + ], + ); + } +} + +class phase extends StatelessWidget { + const phase({ + super.key, + required this.totalVoltage, + required this.totalCurrent, + required this.totalActive, + required this.totalFactor, + }); + + final String totalVoltage; + final String totalCurrent; + final String totalActive; + final String totalFactor; + + @override + Widget build(BuildContext context) { + return Container( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PowerClampInfoCard( + iconPath: Assets.voltageIcon, + title: 'Voltage', + value: totalVoltage, + unit: '', + ), + PowerClampInfoCard( + iconPath: Assets.voltMeterIcon, + title: 'Current', + value: totalCurrent, + unit: '', + ), + PowerClampInfoCard( + iconPath: Assets.powerActiveIcon, + title: 'Active Power', + value: totalActive, + unit: '', + ), + PowerClampInfoCard( + iconPath: Assets.speedoMeter, + title: 'Power Factor', + value: totalFactor, + unit: '', + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/view/power_chart.dart b/lib/pages/device_managment/power_clamp/view/power_chart.dart new file mode 100644 index 00000000..19050b8a --- /dev/null +++ b/lib/pages/device_managment/power_clamp/view/power_chart.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EnergyConsumptionPage extends StatefulWidget { + final List chartData; + final double totalConsumption; + final String date; + final String formattedDate; + final Widget widget; + final Function()? onTap; + + EnergyConsumptionPage({ + required this.chartData, + required this.totalConsumption, + required this.date, + required this.widget, + required this.onTap, + required this.formattedDate, + }); + + @override + _EnergyConsumptionPageState createState() => _EnergyConsumptionPageState(); +} + +class _EnergyConsumptionPageState extends State { + late List _chartData; + + @override + void initState() { + _chartData = widget.chartData; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + color: ColorsManager.whiteColors, + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total Consumption', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + ), + ), + Text( + '8623.20 kWh', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + ), + ), + ], + ), + const Row( + children: [ + Text( + 'Energy consumption', + style: TextStyle( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.formattedDate, + style: const TextStyle( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + fontSize: 8, + ), + ), + const Text( + '1000.00 kWh', + style: TextStyle( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + fontSize: 8, + ), + ), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 10), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.11, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + handleBuiltInTouches: true, + touchSpotThreshold: 2, + getTouchLineEnd: (barData, spotIndex) { + return 10.0; + }, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchTooltipItem) => Colors.white, + tooltipRoundedRadius: 10.0, + tooltipPadding: const EdgeInsets.all(8.0), + tooltipBorder: const BorderSide( + color: ColorsManager.grayColor, width: 1), + getTooltipItems: (List touchedSpots) { + return touchedSpots.map((spot) { + return LineTooltipItem( + '${spot.x},\n ${spot.y.toStringAsFixed(2)} kWh', + const TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ); + }).toList(); + }, + )), + titlesData: FlTitlesData( + bottomTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: false, + reservedSize: 70, + getTitlesWidget: (value, meta) { + int index = value.toInt(); + if (index >= 0 && index < _chartData.length) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: RotatedBox( + quarterTurns: -1, + child: Text(_chartData[index].time, + style: TextStyle(fontSize: 10)), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: true, + horizontalInterval: 1, + verticalInterval: 1, + getDrawingVerticalLine: (value) { + return FlLine( + color: Colors.grey.withOpacity(0.2), + dashArray: [8, 8], + strokeWidth: 1, + ); + }, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.withOpacity(0.2), + dashArray: [5, 5], + strokeWidth: 1, + ); + }, + drawHorizontalLine: false, + ), + lineBarsData: [ + LineChartBarData( + preventCurveOvershootingThreshold: 0.1, + curveSmoothness: 0.5, + preventCurveOverShooting: true, + aboveBarData: BarAreaData(), + spots: _chartData + .asMap() + .entries + .map((entry) => FlSpot(entry.key.toDouble(), + entry.value.consumption)) + .toList(), + isCurved: true, + color: ColorsManager.primaryColor.withOpacity(0.6), + show: true, + shadow: const Shadow(color: Colors.black12), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + ColorsManager.primaryColor.withOpacity(0.5), + Colors.blue.withOpacity(0.1), + ], + begin: Alignment.center, + end: Alignment.bottomCenter, + ), + ), + dotData: const FlDotData( + show: false, + ), + isStrokeCapRound: true, + barWidth: 2, + ), + ], + borderData: FlBorderData( + show: false, + border: Border.all( + color: Color(0xff023DFE).withOpacity(0.7), + width: 10, + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(10), + ), + child: Container(child: widget.widget), + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + child: Container( + padding: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(10), + ), + child: InkWell( + onTap: widget.onTap, + child: Center( + child: SizedBox( + child: Padding( + padding: const EdgeInsets.all(5), + child: Text(widget.date), + ), + ), + ), + ), + ), + ), + ], + ), + ) + ], + ), + ], + ), + ); + } +} + +class EnergyData { + EnergyData(this.time, this.consumption); + final String time; + final double consumption; +} diff --git a/lib/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart b/lib/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart new file mode 100644 index 00000000..c0244845 --- /dev/null +++ b/lib/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_event.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_state.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class PowerClampBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + final List deviceIds; + + const PowerClampBatchControlView({Key? key, required this.deviceIds}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SmartPowerBloc(deviceId: deviceIds.first) + ..add(SmartPowerFetchBatchEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is SmartPowerLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is SmartPowerLoadBatchControll) { + return _buildStatusControls(context, state.status); + } else if (state is SmartPowerError) { + return Center(child: Text('Error: ${state.message}')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, PowerClampBatchModel status) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + // height: 140, + child: FirmwareUpdateWidget(deviceId: deviceIds.first, version: 2)), + const SizedBox( + width: 12, + ), + SizedBox( + width: 170, + height: 140, + child: FactoryResetWidget( + callFactoryReset: () { + context.read().add(SmartPowerFactoryReset( + deviceId: deviceIds.first, + factoryReset: FactoryResetModel(devicesUuid: deviceIds))); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/view/power_info_card.dart b/lib/pages/device_managment/power_clamp/view/power_info_card.dart new file mode 100644 index 00000000..601b6346 --- /dev/null +++ b/lib/pages/device_managment/power_clamp/view/power_info_card.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PowerClampInfoCard extends StatelessWidget { + final String iconPath; + final String title; + final String value; + final String unit; + + const PowerClampInfoCard({ + Key? key, + required this.iconPath, + required this.title, + required this.value, + required this.unit, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + ), + height: 55, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + width: 16, + ), + SvgPicture.asset( + iconPath, + fit: BoxFit.fill, + ), + const SizedBox( + width: 18, + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 8, + fontWeight: FontWeight.w400, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + value, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + Text( + unit, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart new file mode 100644 index 00000000..03d649fa --- /dev/null +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_event.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/bloc/smart_power_state.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/phase_widget.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_chart.dart'; +import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_info_card.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +//Smart Power Clamp +class SmartPowerDeviceControl extends StatelessWidget + with HelperResponsiveLayout { + final String deviceId; + + const SmartPowerDeviceControl({super.key, required this.deviceId}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SmartPowerBloc(deviceId: deviceId) + ..add(SmartPowerFetchDeviceEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + final _blocProvider = BlocProvider.of(context); + + if (state is SmartPowerLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is FakeState) { + return _buildStatusControls( + currentPage: _blocProvider.currentPage, + context: context, + blocProvider: _blocProvider, + ); + } else if (state is GetDeviceStatus) { + return _buildStatusControls( + currentPage: _blocProvider.currentPage, + context: context, + blocProvider: _blocProvider, + ); + } else if (state is FilterRecordsState) { + return _buildStatusControls( + currentPage: _blocProvider.currentPage, + context: context, + blocProvider: _blocProvider, + ); + } + return const Center(child: CircularProgressIndicator()); + // } + }, + ), + ); + } + + Widget _buildStatusControls({ + required BuildContext context, + required SmartPowerBloc blocProvider, + required int currentPage, + }) { + PageController _pageController = PageController(initialPage: currentPage); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 50), + child: DeviceControlsContainer( + child: Column( + children: [ + const Row( + children: [ + Text( + 'Live', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w700, + color: ColorsManager.textPrimaryColor), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PowerClampInfoCard( + iconPath: Assets.powerActiveIcon, + title: 'Active', + value: blocProvider + .deviceStatus.status.general.dataPoints[2].value + .toString(), + unit: '', + ), + PowerClampInfoCard( + iconPath: Assets.voltMeterIcon, + title: 'Current', + value: blocProvider + .deviceStatus.status.general.dataPoints[1].value + .toString(), + unit: ' A', + ), + PowerClampInfoCard( + iconPath: Assets.frequencyIcon, + title: 'Frequency', + value: blocProvider + .deviceStatus.status.general.dataPoints[4].value + .toString(), + unit: ' Hz', + ), + ], + ), + ), + PhaseWidget( + phaseData: blocProvider.phaseData, + ), + const SizedBox( + height: 10, + ), + Container( + padding: const EdgeInsets.only( + top: 10, + left: 20, + right: 20, + bottom: 10, + ), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + ), + height: 300, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(20), + ), + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.arrow_left), + onPressed: () { + blocProvider.add(SmartPowerArrowPressedEvent(-1)); + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + Text( + currentPage == 0 + ? 'Total' + : currentPage == 1 + ? 'Phase A' + : currentPage == 2 + ? 'Phase B' + : 'Phase C', + style: const TextStyle(fontSize: 18), + ), + IconButton( + icon: const Icon(Icons.arrow_right), + onPressed: () { + blocProvider.add(SmartPowerArrowPressedEvent(1)); + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 5, + ), + Expanded( + flex: 2, + child: PageView( + controller: _pageController, + onPageChanged: (int page) { + blocProvider.add(SmartPowerPageChangedEvent(page)); + }, + physics: const NeverScrollableScrollPhysics(), + children: [ + EnergyConsumptionPage( + formattedDate: + '${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}', + onTap: () { + blocProvider.add(SelectDateEvent(context: context)); + blocProvider.add(FilterRecordsByDateEvent( + selectedDate: blocProvider.dateTime!, + viewType: blocProvider + .views[blocProvider.currentIndex])); + }, + widget: blocProvider.dateSwitcher(), + chartData: blocProvider.energyDataList.isNotEmpty + ? blocProvider.energyDataList + : [ + EnergyData('12:00 AM', 4.0), + EnergyData('01:00 AM', 3.5), + EnergyData('02:00 AM', 3.8), + EnergyData('03:00 AM', 3.2), + EnergyData('04:00 AM', 4.0), + EnergyData('05:00 AM', 3.4), + EnergyData('06:00 AM', 3.2), + EnergyData('07:00 AM', 3.5), + EnergyData('08:00 AM', 3.8), + EnergyData('09:00 AM', 3.6), + EnergyData('10:00 AM', 3.9), + EnergyData('11:00 AM', 4.0), + ], + totalConsumption: 10000, + date: blocProvider.formattedDate, + ), + EnergyConsumptionPage( + formattedDate: + '${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}', + onTap: () { + blocProvider.add(SelectDateEvent(context: context)); + }, + widget: blocProvider.dateSwitcher(), + chartData: blocProvider.energyDataList.isNotEmpty + ? blocProvider.energyDataList + : [ + EnergyData('12:00 AM', 4.0), + EnergyData('01:00 AM', 3.5), + EnergyData('02:00 AM', 3.8), + EnergyData('03:00 AM', 3.2), + EnergyData('04:00 AM', 4.0), + EnergyData('05:00 AM', 3.4), + EnergyData('06:00 AM', 3.2), + EnergyData('07:00 AM', 3.5), + EnergyData('08:00 AM', 3.8), + EnergyData('09:00 AM', 3.6), + EnergyData('10:00 AM', 3.9), + EnergyData('11:00 AM', 4.0), + ], + totalConsumption: 10000, + date: blocProvider.formattedDate, + ), + EnergyConsumptionPage( + formattedDate: + '${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}', + onTap: () { + blocProvider.add(SelectDateEvent(context: context)); + }, + widget: blocProvider.dateSwitcher(), + chartData: blocProvider.energyDataList.isNotEmpty + ? blocProvider.energyDataList + : [ + EnergyData('12:00 AM', 4.0), + EnergyData('01:00 AM', 6.5), + EnergyData('02:00 AM', 3.8), + EnergyData('03:00 AM', 3.2), + EnergyData('04:00 AM', 6.0), + EnergyData('05:00 AM', 3.4), + EnergyData('06:00 AM', 5.2), + EnergyData('07:00 AM', 3.5), + EnergyData('08:00 AM', 3.8), + EnergyData('09:00 AM', 5.6), + EnergyData('10:00 AM', 6.9), + EnergyData('11:00 AM', 6.0), + ], + totalConsumption: 10000, + date: blocProvider.formattedDate, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/shared/batch_control/factory_reset.dart b/lib/pages/device_managment/shared/batch_control/factory_reset.dart new file mode 100644 index 00000000..8d1ba3d6 --- /dev/null +++ b/lib/pages/device_managment/shared/batch_control/factory_reset.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class FactoryResetWidget extends StatefulWidget { + const FactoryResetWidget({super.key, required this.callFactoryReset}); + + final Function() callFactoryReset; + + @override + State createState() => _FactoryResetWidgetState(); +} + +class _FactoryResetWidgetState extends State { + bool _showConfirmation = false; + + void _toggleConfirmation() { + setState(() { + _showConfirmation = !_showConfirmation; + }); + } + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + child: _showConfirmation + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Factory Reset', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Are you sure?', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Flexible( + child: DefaultButton( + height: 20, + elevation: 0, + padding: 0, + onPressed: _toggleConfirmation, + backgroundColor: ColorsManager.greyColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ), + const SizedBox(width: 8), + Flexible( + child: DefaultButton( + height: 20, + elevation: 0, + padding: 0, + onPressed: widget.callFactoryReset, + backgroundColor: ColorsManager.red, + child: Text( + 'Reset', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.whiteColors, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ), + ], + ), + ], + ) + : GestureDetector( + onTap: _toggleConfirmation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ClipOval( + child: Container( + color: ColorsManager.whiteColors, + height: 60, + width: 60, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset( + Assets.factoryReset, + fit: BoxFit.cover, + ), + ), + ), + ), + Text( + 'Factory Reset', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/shared/batch_control/firmware_update.dart b/lib/pages/device_managment/shared/batch_control/firmware_update.dart new file mode 100644 index 00000000..e99ee948 --- /dev/null +++ b/lib/pages/device_managment/shared/batch_control/firmware_update.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class FirmwareUpdateWidget extends StatefulWidget { + const FirmwareUpdateWidget({super.key, required this.deviceId, required this.version}); + + final String deviceId; + final int version; + + @override + State createState() => _FirmwareUpdateWidgetState(); +} + +class _FirmwareUpdateWidgetState extends State { + bool _showConfirmation = false; + + void _toggleConfirmation() { + setState(() { + _showConfirmation = !_showConfirmation; + }); + } + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + child: _showConfirmation + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Text( + 'Firmware Update', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Are you sure?', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + ], + ), + Row( + children: [ + Flexible( + child: DefaultButton( + height: 20, + elevation: 0, + padding: 0, + onPressed: _toggleConfirmation, + backgroundColor: ColorsManager.greyColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ), + const SizedBox(width: 8), + Flexible( + child: DefaultButton( + height: 20, + elevation: 0, + padding: 0, + onPressed: () { + _toggleConfirmation(); + }, + backgroundColor: ColorsManager.primaryColor, + child: Text( + 'Update', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.whiteColors, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ), + ], + ), + ], + ) + : GestureDetector( + onTap: _toggleConfirmation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ClipOval( + child: Container( + color: ColorsManager.whiteColors, + height: 60, + width: 60, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset( + Assets.firmware, + fit: BoxFit.cover, + ), + ), + ), + ), + Text( + 'Firmware Update', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/shared/device_batch_control_dialog.dart b/lib/pages/device_managment/shared/device_batch_control_dialog.dart new file mode 100644 index 00000000..f2dc68f5 --- /dev/null +++ b/lib/pages/device_managment/shared/device_batch_control_dialog.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/helper/route_controls_based_code.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCode { + final List devices; + + const DeviceBatchControlDialog({super.key, required this.devices}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: devices.length < 2 ? 500 : 800, + // height: context.screenHeight * 0.7, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Column( + children: [ + Text( + getBatchDialogName(devices.first), + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 8, + ), + Text( + "Batch Control", + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.dialogBlueTitle, + ), + ), + ], + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: const EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + + const SizedBox(height: 20), + //// BUILD DEVICE CONTROLS + /// + //// ROUTE TO SPECIFIC CONTROL VIEW BASED ON DEVICE CATEGORY + routeBatchControlsWidgets(devices: devices), + ], + ), + ), + ), + ), + ); + } +} + +String getBatchDialogName(AllDevicesModel device) { + /* +3G: + 1G: + 2G: + GW: + DL: + WPS: + CPS: + AC: + CUR: + WH: + */ + switch (device.productType) { + case '1G': + return "Smart Light Switch"; + case '2G': + return "Smart Light Switch"; + case '3G': + return "Smart Light Switch"; + case 'GW': + return "Gateway"; + case 'DL': + return "Door Lock"; + case 'WPS': + return "White Presence Sensor"; + case 'CPS': + return "Black Presence Sensor"; + case 'CUR': + return "Smart Curtains"; + case 'WH': + return "Smart Water Heater"; + case 'AC': + return "Smart AC"; + case 'DS': + return "Door / Window Sensor"; + case '1GT': + return "Touch Switch"; + case '2GT': + return "Touch Switch"; + case '3GT': + return "Touch Switch"; + case 'GD': + return "Garage Door Opener"; + case 'WL': + return "Water Leak Sensor"; + case 'SOS': + return "SOS"; + default: + return device.categoryName ?? 'Device Control'; + } +} diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index d115f57e..aa1153af 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -1,11 +1,9 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - -import 'package:syncrow_web/core/extension/build_context_x.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/helper/route_controls_based_code.dart'; - import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { final AllDevicesModel device; @@ -22,7 +20,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), child: SizedBox( width: 798, - height: context.screenHeight * 0.7, + //height: context.screenHeight * 0.7, child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(20.0), @@ -34,7 +32,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { children: [ const SizedBox(), Text( - device.categoryName ?? 'Device Control', + getBatchDialogName(device), style: TextStyle( fontWeight: FontWeight.bold, fontSize: 22, @@ -67,7 +65,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), const SizedBox(height: 20), _buildDeviceInfoSection(), - const SizedBox(height: 20), + //const SizedBox(height: 20), //// BUILD DEVICE CONTROLS /// //// ROUTE TO SPECIFIC CONTROL VIEW BASED ON DEVICE CATEGORY @@ -87,31 +85,50 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { children: [ TableRow( children: [ - _buildInfoRow('Product Name:', device.categoryName ?? 'N/A'), + _buildInfoRow('Product Name:', device.productName ?? 'N/A'), _buildInfoRow('Device ID:', device.uuid ?? ''), ], ), TableRow(children: [ - _buildInfoRow('Virtual Address:', - 'Area - Street 1 - Building 1 - First Floor'), + _buildInfoRow('Virtual Address:', device.ip ?? '-'), const SizedBox.shrink(), ]), TableRow( children: [ - _buildInfoRow('Unit Name:', device.unit?.name ?? 'N/A'), + _buildInfoRow('Space Name:', device.unit?.name ?? 'N/A'), _buildInfoRow('Room:', device.room?.name ?? 'N/A'), ], ), TableRow( children: [ - _buildInfoRow('Installation Date and Time:', '09/08/2024 13:30'), - const SizedBox.shrink(), + _buildInfoRow( + 'Installation Date and Time:', + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + ((device.createTime ?? 0) * 1000), + ), + ), + ), + _buildInfoRow( + 'Battery Level:', + device.batteryLevel != null ? '${device.batteryLevel ?? 0}%' : "-", + statusColor: device.batteryLevel != null + ? (device.batteryLevel! < 20 ? ColorsManager.red : ColorsManager.green) + : null, + ), ], ), TableRow( children: [ _buildInfoRow('Status:', 'Online', statusColor: Colors.green), - _buildInfoRow('Last Offline Date and Time:', '-'), + _buildInfoRow( + 'Last Offline Date and Time:', + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + ((device.updateTime ?? 0) * 1000), + ), + ), + ), ], ), ], diff --git a/lib/pages/device_managment/shared/device_controls_container.dart b/lib/pages/device_managment/shared/device_controls_container.dart index b60a958f..888563da 100644 --- a/lib/pages/device_managment/shared/device_controls_container.dart +++ b/lib/pages/device_managment/shared/device_controls_container.dart @@ -1,20 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; class DeviceControlsContainer extends StatelessWidget { - const DeviceControlsContainer({required this.child, super.key}); + const DeviceControlsContainer({required this.child, this.padding, super.key}); final Widget child; + final double? padding; @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( + return Card( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), - color: ColorsManager.greyColor.withOpacity(0.2), - border: Border.all(color: ColorsManager.boxDivider), ), - padding: const EdgeInsets.all(12), - child: child, + elevation: 3, + surfaceTintColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(20), + ), + padding: + EdgeInsets.symmetric(vertical: padding ?? 10, horizontal: padding ?? 16), //EdgeInsets.all(padding ?? 12), + child: child, + ), ); } } diff --git a/lib/pages/device_managment/shared/icon_name_status_container.dart b/lib/pages/device_managment/shared/icon_name_status_container.dart new file mode 100644 index 00000000..13b6b594 --- /dev/null +++ b/lib/pages/device_managment/shared/icon_name_status_container.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class IconNameStatusContainer extends StatelessWidget { + const IconNameStatusContainer({ + super.key, + required this.name, + required this.icon, + required this.onTap, + required this.status, + required this.textColor, + this.paddingAmount = 12, + required this.isFullIcon, + }); + + final String name; + final String icon; + final GestureTapCallback onTap; + final bool status; + final Color textColor; + final double? paddingAmount; + final bool isFullIcon; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isFullIcon) + ClipOval( + child: SvgPicture.asset( + icon, + fit: BoxFit.contain, + ), + ) + else + ClipOval( + child: Container( + height: 60, + width: 60, + padding: EdgeInsets.all(paddingAmount ?? 8), + color: ColorsManager.whiteColors, + child: SvgPicture.asset( + icon, + width: 35, + height: 35, + fit: BoxFit.contain, + ), + )), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + name, + textAlign: TextAlign.start, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: textColor, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/shared/navigate_home_grid_view.dart b/lib/pages/device_managment/shared/navigate_home_grid_view.dart new file mode 100644 index 00000000..7969e2b0 --- /dev/null +++ b/lib/pages/device_managment/shared/navigate_home_grid_view.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/constants/routes_const.dart'; + +class NavigateHomeGridView extends StatelessWidget { + const NavigateHomeGridView({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () { + context.go(RoutesConst.home); + }, + child: SvgPicture.asset( + height: 20, + width: 20, + Assets.grid, + ), + ), + const SizedBox( + width: 10, + ) + ], + ); + } +} diff --git a/lib/pages/device_managment/shared/sensors_widgets/presence_space_type.dart b/lib/pages/device_managment/shared/sensors_widgets/presence_space_type.dart index 185144e1..e1ca0586 100644 --- a/lib/pages/device_managment/shared/sensors_widgets/presence_space_type.dart +++ b/lib/pages/device_managment/shared/sensors_widgets/presence_space_type.dart @@ -1,21 +1,33 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/core/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; class PresenceSpaceType extends StatelessWidget { const PresenceSpaceType({ super.key, - required this.listOfIcons, required this.description, + required this.value, + required this.action, }); - final List listOfIcons; final String description; + final SpaceTypes value; + final void Function(String value) action; @override Widget build(BuildContext context) { + final Map spaceTypeIcons = { + SpaceTypes.none: Assets.office, + SpaceTypes.parlour: Assets.parlour, + SpaceTypes.area: Assets.dyi, + SpaceTypes.toilet: Assets.bathroom, + SpaceTypes.bedroom: Assets.bedroom, + }; + return DeviceControlsContainer( child: Column( mainAxisSize: MainAxisSize.min, @@ -35,13 +47,29 @@ class PresenceSpaceType extends StatelessWidget { Wrap( runSpacing: 8, spacing: 16, - children: [ - ...listOfIcons.map((icon) => SvgPicture.asset( + children: spaceTypeIcons.entries.map((entry) { + final icon = entry.value; + final spaceType = entry.key; + return GestureDetector( + onTap: () => action(spaceType.name), + child: Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: value == spaceType + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + child: SvgPicture.asset( icon, - width: 40, - height: 40, - )), - ], + width: 25, + height: 22, + ), + ), + ); + }).toList(), ), ], ), diff --git a/lib/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart b/lib/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart index 6d37ec3a..4e64ee1e 100644 --- a/lib/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart +++ b/lib/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart @@ -23,6 +23,8 @@ class PresenceNoBodyTime extends StatefulWidget { class _PresenceUpdateDataState extends State { late String _currentValue; + late String _numericValue; + late String _unit; final List nobodyTimeRange = [ 'none', @@ -40,29 +42,45 @@ class _PresenceUpdateDataState extends State { void initState() { super.initState(); _currentValue = widget.value; + _numericValue = _extractNumericValue(_currentValue); + _unit = _extractUnit(_currentValue); + } + + String _extractNumericValue(String value) { + if (value == 'none') return '0'; + return value.replaceAll(RegExp(r'[a-zA-Z]'), '').trim(); + } + + String _extractUnit(String value) { + if (value == 'none') return ''; + if (value.endsWith('s')) return 's'; + if (value.endsWith('min')) return 'min'; + if (value.endsWith('hour')) return 'hr'; + return ''; } void _onValueChanged(String newValue) { + setState(() { + _currentValue = newValue; + _numericValue = _extractNumericValue(newValue); + _unit = _extractUnit(newValue); + }); widget.action(newValue); } void _incrementValue() { int currentIndex = nobodyTimeRange.indexOf(_currentValue); if (currentIndex < nobodyTimeRange.length - 1) { - setState(() { - _currentValue = nobodyTimeRange[currentIndex + 1]; - }); - _onValueChanged(_currentValue); + String newValue = nobodyTimeRange[currentIndex + 1]; + _onValueChanged(newValue); } } void _decrementValue() { int currentIndex = nobodyTimeRange.indexOf(_currentValue); if (currentIndex > 0) { - setState(() { - _currentValue = nobodyTimeRange[currentIndex - 1]; - }); - _onValueChanged(_currentValue); + String newValue = nobodyTimeRange[currentIndex - 1]; + _onValueChanged(newValue); } } @@ -81,11 +99,12 @@ class _PresenceUpdateDataState extends State { fontSize: 10), ), IncrementDecrementWidget( - value: _currentValue, - description: widget.description ?? '', - descriptionColor: ColorsManager.blackColor, - onIncrement: _incrementValue, - onDecrement: _decrementValue), + value: _numericValue, + description: _unit, + descriptionColor: ColorsManager.blackColor, + onIncrement: _incrementValue, + onDecrement: _decrementValue, + ), ], ), ); diff --git a/lib/pages/device_managment/shared/table/report_table.dart b/lib/pages/device_managment/shared/table/report_table.dart index 619df168..11385080 100644 --- a/lib/pages/device_managment/shared/table/report_table.dart +++ b/lib/pages/device_managment/shared/table/report_table.dart @@ -3,78 +3,120 @@ import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/table_cell_widget.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/table_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +// ignore: must_be_immutable class ReportsTable extends StatelessWidget { final DeviceReport report; + final String? thirdColumnTitle; + final String? thirdColumnDescription; final Function(int index) onRowTap; final VoidCallback onClose; + bool? hideValueShowDescription; + bool? mainDoorSensor; + bool? garageDoorSensor; + bool? waterLeak; - const ReportsTable({ + ReportsTable({ super.key, required this.report, required this.onRowTap, required this.onClose, + this.thirdColumnTitle, + this.thirdColumnDescription, + this.hideValueShowDescription, + this.mainDoorSensor, + this.garageDoorSensor, + this.waterLeak, }); @override Widget build(BuildContext context) { - return Stack( - children: [ - Padding( - padding: const EdgeInsets.all(20.0), - child: Table( - border: TableBorder.all(color: Colors.grey.shade300, width: 1), - columnWidths: const { - 0: FlexColumnWidth(), - 1: FlexColumnWidth(), - 2: FlexColumnWidth(), - }, - children: [ - TableRow( - decoration: BoxDecoration(color: Colors.grey.shade200), - children: const [ - TableHeader(title: 'Date'), - TableHeader(title: 'Time'), - TableHeader(title: 'Status'), - ], - ), - ...report.data!.asMap().entries.map((entry) { - int index = entry.key; - DeviceEvent data = entry.value; - - // Parse eventTime into Date and Time - DateTime eventDateTime = - DateTime.fromMillisecondsSinceEpoch(data.eventTime!); - String date = DateFormat('dd/MM/yyyy').format(eventDateTime); - String time = DateFormat('HH:mm').format(eventDateTime); - - return TableRow( - children: [ - TableCellWidget(value: date), - TableCellWidget(value: time), - TableCellWidget( - value: data.value!, - onTap: () => onRowTap(index), - ), - ], - ); - }).toList(), - ], - ), - ), - Positioned( - top: 0, - right: 0, - child: IconButton( - icon: const Icon( - Icons.close, - color: Colors.red, - size: 18, + return report.data == null || report.data!.isEmpty + ? Container( + padding: const EdgeInsets.all(20.0), + width: MediaQuery.sizeOf(context).width, + alignment: AlignmentDirectional.center, + height: 100, + child: Text( + 'No reports found', + style: context.textTheme.bodyLarge!.copyWith(color: ColorsManager.grayColor), ), - onPressed: onClose, - ), - ), - ], - ); + ) + : Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Table( + border: TableBorder.all(color: Colors.grey.shade300, width: 1), + columnWidths: const { + 0: FlexColumnWidth(), + 1: FlexColumnWidth(), + 2: FlexColumnWidth(), + }, + children: [ + TableRow( + decoration: BoxDecoration(color: Colors.grey.shade200), + children: [ + const TableHeader(title: 'Date'), + const TableHeader(title: 'Time'), + TableHeader(title: thirdColumnTitle ?? 'Status'), + ], + ), + if (report.data != null) + ...report.data!.asMap().entries.map((entry) { + int index = entry.key; + DeviceEvent data = entry.value; + + // Parse eventTime into Date and Time + DateTime eventDateTime = + DateTime.fromMillisecondsSinceEpoch(data.eventTime!); + String date = DateFormat('dd/MM/yyyy').format(eventDateTime); + String time = DateFormat('HH:mm').format(eventDateTime); + + String value; + if (hideValueShowDescription == true) { + if (mainDoorSensor != null && mainDoorSensor == true) { + value = data.value == 'true' ? 'Open' : 'Close'; + } else if (garageDoorSensor != null && garageDoorSensor == true) { + value = data.value == 'true' ? 'Opened' : 'Closed'; + } else if (waterLeak != null && waterLeak == true) { + value = data.value == 'normal' ? 'Normal' : 'Leak Detected'; + } else { + value = '${data.value!} ${thirdColumnDescription ?? ''}'; + } + } else { + value = '${data.value!} ${thirdColumnDescription ?? ''}'; + } + + return TableRow( + children: [ + TableCellWidget(value: date), + TableCellWidget(value: time), + TableCellWidget( + value: value, + onTap: () => onRowTap(index), + ), + ], + ); + }) + ], + ), + ), + Positioned( + top: 0, + right: 0, + child: IconButton( + icon: const Icon( + Icons.close, + color: Colors.red, + size: 18, + ), + onPressed: onClose, + ), + ), + ], + ); } } diff --git a/lib/pages/device_managment/shared/toggle_widget.dart b/lib/pages/device_managment/shared/toggle_widget.dart new file mode 100644 index 00000000..ad0ba8ad --- /dev/null +++ b/lib/pages/device_managment/shared/toggle_widget.dart @@ -0,0 +1,89 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ToggleWidget extends StatelessWidget { + final bool value; + final String code; + final String deviceId; + final String label; + final String? icon; + final Widget? labelWidget; + final Function(dynamic value) onChange; + final bool showToggle; + final bool showIcon; + + const ToggleWidget({ + super.key, + required this.value, + required this.code, + required this.deviceId, + required this.label, + required this.onChange, + this.icon, + this.labelWidget, + this.showToggle = true, + this.showIcon = true, + }); + + @override + Widget build(BuildContext context) { + return DeviceControlsContainer( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + icon == '-1' + ? const SizedBox( + height: 60, + width: 60, + ) + : ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.whiteColors, + child: SvgPicture.asset( + icon ?? Assets.lightPulp, + width: 35, + height: 35, + fit: BoxFit.contain, + ), + )), + if (showToggle) + Container( + height: 20, + width: 35, + padding: const EdgeInsets.only(right: 16, top: 10), + child: CupertinoSwitch( + value: value, + activeColor: ColorsManager.dialogBlueTitle, + onChanged: onChange, + ), + ), + ], + ), + ), + labelWidget ?? + Text( + label, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/sos/bloc/sos_device_bloc.dart b/lib/pages/device_managment/sos/bloc/sos_device_bloc.dart new file mode 100644 index 00000000..14fdf61c --- /dev/null +++ b/lib/pages/device_managment/sos/bloc/sos_device_bloc.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/sos/models/sos_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'sos_device_event.dart'; +part 'sos_device_state.dart'; + +class SosDeviceBloc extends Bloc { + SosDeviceBloc() : super(SosDeviceInitial()) { + on(_getDeviceStatus); + on(_getBatchStatus); + on(_getDeviceRecords); + on(_getDeviceAutomationRecords); + on(_backToSosStatusView); + on(_sosFactoryReset); + } + + late SosStatusModel deviceStatus; + + FutureOr _getDeviceStatus(GetDeviceStatus event, Emitter emit) async { + emit(SosDeviceLoadingState()); + try { + final status = await DevicesManagementApi().getDeviceStatus(event.uuid); + deviceStatus = SosStatusModel.fromJson(event.uuid, status.status); + emit(SosDeviceLoadedState(deviceStatus)); + } catch (e) { + emit(SosDeviceErrorState(e.toString())); + } + } + + FutureOr _getBatchStatus(GetBatchStatus event, Emitter emit) async { + emit(SosDeviceLoadingState()); + try { + final status = await DevicesManagementApi().getBatchStatus(event.uuids); + deviceStatus = SosStatusModel.fromJson(event.uuids.first, status.status); + emit(SosDeviceLoadedState(deviceStatus)); + } catch (e) { + emit(SosDeviceErrorState(e.toString())); + } + } + + FutureOr _getDeviceRecords(GetDeviceRecords event, Emitter emit) async { + emit(SosReportLoadingState()); + try { + final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; + final to = DateTime.now().millisecondsSinceEpoch; + final DeviceReport records = + await DevicesManagementApi.getDeviceReportsByDate(event.uuid, 'sos', from.toString(), to.toString()); + emit(SosReportLoadedState(records)); + } catch (e) { + emit(SosReportErrorState(e.toString())); + } + } + + FutureOr _getDeviceAutomationRecords(GetDeviceAutomationRecords event, Emitter emit) async { + emit(SosAutomationReportLoadingState()); + try { + final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; + final to = DateTime.now().millisecondsSinceEpoch; + final DeviceReport records = await DevicesManagementApi.getDeviceReportsByDate( + event.uuid, 'sos_automation', from.toString(), to.toString()); + emit(SosAutomationReportLoadedState(records)); + } catch (e) { + emit(SosAutomationReportErrorState(e.toString())); + } + } + + FutureOr _backToSosStatusView(BackToSosStatusView event, Emitter emit) { + emit(SosDeviceLoadedState(deviceStatus)); + } + + FutureOr _sosFactoryReset(SosFactoryReset event, Emitter emit) async { + emit(SosDeviceLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset(event.factoryReset, event.deviceId); + if (response) { + emit(SosDeviceLoadedState(deviceStatus)); + } else { + emit(const SosDeviceErrorState('Factory reset failed')); + } + } catch (e) { + emit(SosDeviceErrorState(e.toString())); + } + } +} diff --git a/lib/pages/device_managment/sos/bloc/sos_device_event.dart b/lib/pages/device_managment/sos/bloc/sos_device_event.dart new file mode 100644 index 00000000..270360b8 --- /dev/null +++ b/lib/pages/device_managment/sos/bloc/sos_device_event.dart @@ -0,0 +1,49 @@ +part of 'sos_device_bloc.dart'; + +sealed class SosDeviceEvent extends Equatable { + const SosDeviceEvent(); +} + +class GetDeviceStatus extends SosDeviceEvent { + final String uuid; + const GetDeviceStatus(this.uuid); + @override + List get props => [uuid]; +} + +class GetBatchStatus extends SosDeviceEvent { + final List uuids; + const GetBatchStatus(this.uuids); + @override + List get props => [uuids]; +} + +class GetDeviceRecords extends SosDeviceEvent { + final String uuid; + + const GetDeviceRecords(this.uuid); + @override + List get props => [uuid]; +} + +class GetDeviceAutomationRecords extends SosDeviceEvent { + final String uuid; + const GetDeviceAutomationRecords(this.uuid); + @override + List get props => [uuid]; +} + +class BackToSosStatusView extends SosDeviceEvent { + @override + List get props => []; +} + +class SosFactoryReset extends SosDeviceEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const SosFactoryReset({required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/sos/bloc/sos_device_state.dart b/lib/pages/device_managment/sos/bloc/sos_device_state.dart new file mode 100644 index 00000000..b6a1eeec --- /dev/null +++ b/lib/pages/device_managment/sos/bloc/sos_device_state.dart @@ -0,0 +1,82 @@ +part of 'sos_device_bloc.dart'; + +sealed class SosDeviceState extends Equatable { + const SosDeviceState(); +} + +final class SosDeviceInitial extends SosDeviceState { + @override + List get props => []; +} + +final class SosDeviceLoadingState extends SosDeviceState { + @override + List get props => []; +} + +final class SosDeviceLoadedState extends SosDeviceState { + final SosStatusModel sosStatusModel; + + const SosDeviceLoadedState(this.sosStatusModel); + + @override + List get props => [sosStatusModel]; +} + +final class SosDeviceErrorState extends SosDeviceState { + final String message; + + const SosDeviceErrorState(this.message); + + @override + List get props => [message]; +} + +/// report state +final class SosReportLoadingState extends SosDeviceState { + @override + List get props => []; +} + +final class SosReportLoadedState extends SosDeviceState { + final DeviceReport sosReport; + + const SosReportLoadedState(this.sosReport); + + @override + List get props => [sosReport]; +} + +final class SosReportErrorState extends SosDeviceState { + final String message; + + const SosReportErrorState(this.message); + + @override + List get props => [message]; +} + +/// automation reports + +final class SosAutomationReportLoadingState extends SosDeviceState { + @override + List get props => []; +} + +final class SosAutomationReportLoadedState extends SosDeviceState { + final DeviceReport automationReport; + + const SosAutomationReportLoadedState(this.automationReport); + + @override + List get props => [automationReport]; +} + +final class SosAutomationReportErrorState extends SosDeviceState { + final String message; + + const SosAutomationReportErrorState(this.message); + + @override + List get props => [message]; +} diff --git a/lib/pages/device_managment/sos/models/sos_status_model.dart b/lib/pages/device_managment/sos/models/sos_status_model.dart new file mode 100644 index 00000000..ac0de3b0 --- /dev/null +++ b/lib/pages/device_managment/sos/models/sos_status_model.dart @@ -0,0 +1,34 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class SosStatusModel { + final int batteryLevel; + final String sosStatus; + final String deviceId; + + SosStatusModel({ + required this.batteryLevel, + required this.sosStatus, + required this.deviceId, + }); + + factory SosStatusModel.fromJson(String deviceId, List statuses) { + late int batteryLevel; + late String sosStatus; + + for (var status in statuses) { + switch (status.code) { + case 'battery_percentage': + batteryLevel = status.value; + break; + case 'sos': + sosStatus = status.value; + break; + } + } + return SosStatusModel( + deviceId: deviceId, + batteryLevel: batteryLevel, + sosStatus: sosStatus, + ); + } +} diff --git a/lib/pages/device_managment/sos/view/sos_batch_control_view.dart b/lib/pages/device_managment/sos/view/sos_batch_control_view.dart new file mode 100644 index 00000000..ec15b7d6 --- /dev/null +++ b/lib/pages/device_managment/sos/view/sos_batch_control_view.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/sos/bloc/sos_device_bloc.dart'; + +class SOSBatchControlView extends StatelessWidget { + const SOSBatchControlView({ + super.key, + required this.deviceIds, + }); + + final List deviceIds; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + // height: 140, + child: FirmwareUpdateWidget(deviceId: deviceIds.first, version: 2)), + const SizedBox( + width: 12, + ), + SizedBox( + width: 170, + height: 140, + child: FactoryResetWidget( + callFactoryReset: () { + context.read().add( + SosFactoryReset(deviceId: deviceIds.first, factoryReset: FactoryResetModel(devicesUuid: deviceIds))); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/sos/view/sos_device_control_view.dart b/lib/pages/device_managment/sos/view/sos_device_control_view.dart new file mode 100644 index 00000000..dff67c55 --- /dev/null +++ b/lib/pages/device_managment/sos/view/sos_device_control_view.dart @@ -0,0 +1,138 @@ +// sos device control view + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; +import 'package:syncrow_web/pages/device_managment/sos/bloc/sos_device_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/sos/widgets/sos_notification_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +import '../models/sos_status_model.dart'; + +class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { + const SosDeviceControlsView({ + super.key, + required this.device, + }); + + final AllDevicesModel device; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SosDeviceBloc()..add(GetDeviceStatus(device.uuid!)), + child: BlocBuilder( + builder: (context, state) { + if (state is SosDeviceLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is SosDeviceLoadedState) { + return _buildStatusControls(context, state.sosStatusModel); + } else if (state is SosReportLoadedState) { + return ReportsTable( + report: state.sosReport, + hideValueShowDescription: true, + garageDoorSensor: true, + onRowTap: (index) {}, + onClose: () { + context.read().add(BackToSosStatusView()); + }, + ); + } else if (state is SosAutomationReportLoadedState) { + return ReportsTable( + report: state.automationReport, + hideValueShowDescription: true, + garageDoorSensor: true, + onRowTap: (index) {}, + onClose: () { + context.read().add(BackToSosStatusView()); + }, + ); + } else if (state is SosDeviceErrorState) { + return const Center(child: Text('Error fetching status')); + } else if (state is SosAutomationReportErrorState) { + return Center(child: Text('Error: ${state.message.toString()}')); + } else if (state is SosReportErrorState) { + return Center(child: Text('Error: ${state.message.toString()}')); + } + return const Center(child: CircularProgressIndicator()); + }, + )); + } + + Widget _buildStatusControls(BuildContext context, SosStatusModel deviceStatus) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + IconNameStatusContainer( + isFullIcon: false, + name: deviceStatus.sosStatus == 'sos' ? 'SOS' : 'Normal', + icon: deviceStatus.sosStatus == 'sos' ? Assets.sos : Assets.sosNormal, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'SOS Records', + icon: Assets.records, + onTap: () { + context.read().add( + GetDeviceRecords( + device.uuid!, + ), + ); + }, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Automation Record', + icon: Assets.automationRecords, + onTap: () { + // context.read().add( + // GetDeviceAutomationRecords( + // device.uuid!, + // ), + // ); + }, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Alarm Settings', + icon: Assets.mainDoorNotifi, + onTap: () { + showDialog( + context: context, + builder: (context) => const SosNotificationDialog(), + ); + }, + status: false, + textColor: ColorsManager.blackColor, + paddingAmount: 14, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/sos/widgets/sos_notification_dialog.dart b/lib/pages/device_managment/sos/widgets/sos_notification_dialog.dart new file mode 100644 index 00000000..55428107 --- /dev/null +++ b/lib/pages/device_managment/sos/widgets/sos_notification_dialog.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SosNotificationDialog extends StatefulWidget { + const SosNotificationDialog({super.key}); + + @override + State createState() => _NotificationDialogState(); +} + +class _NotificationDialogState extends State { + bool isLowBatteryNotificationEnabled = true; + bool isSosAlarmEnabled = true; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 550, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Notification Settings', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isSosAlarmEnabled, + code: 'notification', + deviceId: '', + label: 'SOS Alarm', + onChange: (v) { + setState(() { + isSosAlarmEnabled = v; + }); + }, + icon: '-1', + ), + ), + const SizedBox( + width: 16, + ), + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isLowBatteryNotificationEnabled, + code: 'notification', + deviceId: '', + label: 'Low Battery', + onChange: (v) { + setState(() { + isLowBatteryNotificationEnabled = v; + }); + }, + icon: '-1', + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart new file mode 100644 index 00000000..8e3c109e --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart @@ -0,0 +1,174 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'three_gang_glass_switch_event.dart'; +part 'three_gang_glass_switch_state.dart'; + +class ThreeGangGlassSwitchBloc extends Bloc { + ThreeGangGlassStatusModel deviceStatus; + Timer? _timer; + + ThreeGangGlassSwitchBloc({required String deviceId}) + : deviceStatus = ThreeGangGlassStatusModel( + uuid: deviceId, + switch1: false, + countDown1: 0, + switch2: false, + countDown2: 0, + switch3: false, + countDown3: 0), + super(ThreeGangGlassSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFactoryReset); + } + + Future _onFetchDeviceStatus( + ThreeGangGlassSwitchFetchDeviceEvent event, Emitter emit) async { + emit(ThreeGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status); + emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(ThreeGangGlassSwitchError(e.toString())); + } + } + + Future _onControl(ThreeGangGlassSwitchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _onBatchControl(ThreeGangGlassSwitchBatchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + Future _onFetchBatchStatus( + ThreeGangGlassSwitchFetchBatchStatusEvent event, Emitter emit) async { + emit(ThreeGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); + emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); + } catch (e) { + emit(ThreeGangGlassSwitchError(e.toString())); + } + } + + Future _onFactoryReset(ThreeGangGlassFactoryReset event, Emitter emit) async { + emit(ThreeGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset(event.factoryReset, event.deviceId); + if (!response) { + emit(ThreeGangGlassSwitchError('Failed')); + } else { + emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(ThreeGangGlassSwitchError(e.toString())); + } + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi().deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi().deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _revertValueAndEmit(String deviceId, String code, bool oldValue, Emitter emit) { + _updateLocalValue(code, oldValue); + emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } else if (code == 'switch_2') { + deviceStatus = deviceStatus.copyWith(switch2: value); + } else if (code == 'switch_3') { + deviceStatus = deviceStatus.copyWith(switch3: value); + } + } + + bool _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.switch1; + case 'switch_2': + return deviceStatus.switch2; + case 'switch_3': + return deviceStatus.switch3; + default: + return false; + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart new file mode 100644 index 00000000..558b9824 --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart @@ -0,0 +1,51 @@ +part of 'three_gang_glass_switch_bloc.dart'; + +@immutable +abstract class ThreeGangGlassSwitchEvent {} + +class ThreeGangGlassSwitchFetchDeviceEvent extends ThreeGangGlassSwitchEvent { + final String deviceId; + + ThreeGangGlassSwitchFetchDeviceEvent(this.deviceId); +} + +class ThreeGangGlassSwitchControl extends ThreeGangGlassSwitchEvent { + final String deviceId; + final String code; + final bool value; + + ThreeGangGlassSwitchControl({ + required this.deviceId, + required this.code, + required this.value, + }); +} + +class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { + final List deviceIds; + final String code; + final bool value; + + ThreeGangGlassSwitchBatchControl({ + required this.deviceIds, + required this.code, + required this.value, + }); +} + +class ThreeGangGlassSwitchFetchBatchStatusEvent + extends ThreeGangGlassSwitchEvent { + final List deviceIds; + + ThreeGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); +} + +class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + ThreeGangGlassFactoryReset({ + required this.deviceId, + required this.factoryReset, + }); +} diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_state.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_state.dart new file mode 100644 index 00000000..aedb3a9b --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_state.dart @@ -0,0 +1,32 @@ +part of 'three_gang_glass_switch_bloc.dart'; + +@immutable +abstract class ThreeGangGlassSwitchState {} + +class ThreeGangGlassSwitchInitial extends ThreeGangGlassSwitchState {} + +class ThreeGangGlassSwitchLoading extends ThreeGangGlassSwitchState {} + +class ThreeGangGlassSwitchStatusLoaded extends ThreeGangGlassSwitchState { + final ThreeGangGlassStatusModel status; + + ThreeGangGlassSwitchStatusLoaded(this.status); +} + +class ThreeGangGlassSwitchError extends ThreeGangGlassSwitchState { + final String message; + + ThreeGangGlassSwitchError(this.message); +} + +class ThreeGangGlassSwitchBatchStatusLoaded extends ThreeGangGlassSwitchState { + final ThreeGangGlassStatusModel status; + + ThreeGangGlassSwitchBatchStatusLoaded(this.status); +} + +class ThreeGangGlassSwitchBatchControlError extends ThreeGangGlassSwitchState { + final String message; + + ThreeGangGlassSwitchBatchControlError(this.message); +} diff --git a/lib/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart b/lib/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart new file mode 100644 index 00000000..cec12b7f --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart @@ -0,0 +1,87 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class ThreeGangGlassStatusModel { + final String uuid; + final bool switch1; + final int countDown1; + final bool switch2; + final int countDown2; + final bool switch3; + final int countDown3; + + ThreeGangGlassStatusModel({ + required this.uuid, + required this.switch1, + required this.countDown1, + required this.switch2, + required this.countDown2, + required this.switch3, + required this.countDown3, + }); + + factory ThreeGangGlassStatusModel.fromJson(String id, List jsonList) { + late bool switch1; + late int countDown1; + late bool switch2; + late int countDown2; + late bool switch3; + late int countDown3; + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + switch1 = status.value ?? false; + break; + case 'countdown_1': + countDown1 = status.value ?? 0; + break; + case 'switch_2': + switch2 = status.value ?? false; + break; + case 'countdown_2': + countDown2 = status.value ?? 0; + break; + case 'switch_3': + switch3 = status.value ?? false; + break; + case 'countdown_3': + countDown3 = status.value ?? 0; + break; + } + } + + return ThreeGangGlassStatusModel( + uuid: id, + switch1: switch1, + countDown1: countDown1, + switch2: switch2, + countDown2: countDown2, + switch3: switch3, + countDown3: countDown3, + ); + } + + ThreeGangGlassStatusModel copyWith({ + String? uuid, + bool? switch1, + int? countDown1, + bool? switch2, + int? countDown2, + bool? switch3, + int? countDown3, + }) { + return ThreeGangGlassStatusModel( + uuid: uuid ?? this.uuid, + switch1: switch1 ?? this.switch1, + countDown1: countDown1 ?? this.countDown1, + switch2: switch2 ?? this.switch2, + countDown2: countDown2 ?? this.countDown2, + switch3: switch3 ?? this.switch3, + countDown3: countDown3 ?? this.countDown3, + ); + } + + @override + String toString() => + 'ThreeGangGlassStatusModel(uuid: $uuid, switch1: $switch1, countDown1: $countDown1, switch2: $switch2, countDown2: $countDown2, switch3: $switch3, countDown3: $countDown3)'; +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart new file mode 100644 index 00000000..4d1bb91c --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class ThreeGangGlassSwitchBatchControlView extends StatelessWidget with HelperResponsiveLayout { + final List deviceIds; + + const ThreeGangGlassSwitchBatchControlView({required this.deviceIds, super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ThreeGangGlassSwitchBloc(deviceId: deviceIds.first) + ..add(ThreeGangGlassSwitchFetchBatchStatusEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is ThreeGangGlassSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is ThreeGangGlassSwitchBatchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is ThreeGangGlassSwitchError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, ThreeGangGlassStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: "Wall Light", + onChange: (value) { + context.read().add( + ThreeGangGlassSwitchBatchControl( + deviceIds: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceIds.first, + label: "Ceiling Light", + onChange: (value) { + context.read().add( + ThreeGangGlassSwitchBatchControl( + deviceIds: deviceIds, + code: 'switch_2', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch3, + code: 'switch_3', + deviceId: deviceIds.first, + label: "SpotLight", + onChange: (value) { + context.read().add( + ThreeGangGlassSwitchBatchControl( + deviceIds: deviceIds, + code: 'switch_3', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, // adjust the version according to your requirement + ), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + ThreeGangGlassFactoryReset( + deviceId: status.uuid, + factoryReset: FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart new file mode 100644 index 00000000..433e5408 --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +import '../models/three_gang_glass_switch.dart'; + +class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + + const ThreeGangGlassSwitchControlView({required this.deviceId, super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + ThreeGangGlassSwitchBloc(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is ThreeGangGlassSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is ThreeGangGlassSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is ThreeGangGlassSwitchError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, ThreeGangGlassStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: "Wall Light", + onChange: (value) { + context.read().add( + ThreeGangGlassSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceId, + label: "Ceiling Light", + onChange: (value) { + context.read().add( + ThreeGangGlassSwitchControl( + deviceId: deviceId, + code: 'switch_2', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch3, + code: 'switch_3', + deviceId: deviceId, + label: "SpotLight", + onChange: (value) { + context.read().add( + ThreeGangGlassSwitchControl( + deviceId: deviceId, + code: 'switch_3', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Preferences', + icon: Assets.preferences, + onChange: (value) {}, + showToggle: false, + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Scheduling', + icon: Assets.scheduling, + onChange: (value) {}, + showToggle: false, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart similarity index 52% rename from lib/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart rename to lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart index 8adc11cb..ca264c13 100644 --- a/lib/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart @@ -4,7 +4,8 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/models/living_room_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'living_room_event.dart'; @@ -16,12 +17,15 @@ class LivingRoomBloc extends Bloc { Timer? _timer; LivingRoomBloc({required this.deviceId}) : super(LivingRoomInitial()) { - on(_onFetchDeviceStatus); + on(_onFetchDeviceStatus); on(_livingRoomControl); + on(_livingRoomBatchControl); + on(_livingRoomFetchBatchControl); + on(_livingRoomFactoryReset); } - FutureOr _onFetchDeviceStatus( - LivingRoomFetchDeviceStatus event, Emitter emit) async { + FutureOr _onFetchDeviceStatus(LivingRoomFetchDeviceStatusEvent event, + Emitter emit) async { emit(LivingRoomDeviceStatusLoading()); try { final status = @@ -48,28 +52,44 @@ class LivingRoomBloc extends Bloc { value: event.value, oldValue: oldValue, emit: emit, + isBatch: false, ); } Future _runDebounce({ - required String deviceId, + required dynamic deviceId, required String code, required dynamic value, required dynamic oldValue, required Emitter emit, + required bool isBatch, }) async { + late String id; + + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + if (_timer != null) { _timer!.cancel(); } _timer = Timer(const Duration(seconds: 1), () async { try { - final response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } if (!response) { - _revertValueAndEmit(deviceId, code, oldValue, emit); + _revertValueAndEmit(id, code, oldValue, emit); } } catch (e) { - _revertValueAndEmit(deviceId, code, oldValue, emit); + _revertValueAndEmit(id, code, oldValue, emit); } }); } @@ -78,7 +98,6 @@ class LivingRoomBloc extends Bloc { Emitter emit) { _updateLocalValue(code, oldValue); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - emit(const LivingRoomControlError('Failed to control the device.')); } void _updateLocalValue(String code, dynamic value) { @@ -116,4 +135,54 @@ class LivingRoomBloc extends Bloc { return null; } } + + FutureOr _livingRoomFetchBatchControl( + LivingRoomFetchBatchEvent event, Emitter emit) async { + emit(LivingRoomDeviceStatusLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + LivingRoomStatusModel.fromJson(event.devicesIds.first, status.status); + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + } catch (e) { + emit(LivingRoomDeviceManagementError(e.toString())); + } + } + + FutureOr _livingRoomBatchControl( + LivingRoomBatchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.devicesIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + FutureOr _livingRoomFactoryReset( + LivingRoomFactoryResetEvent event, Emitter emit) async { + emit(LivingRoomDeviceStatusLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.uuid, + ); + if (!response) { + emit(const LivingRoomDeviceManagementError('Failed')); + } else { + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(LivingRoomDeviceManagementError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart new file mode 100644 index 00000000..c0ada0f6 --- /dev/null +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart @@ -0,0 +1,60 @@ +part of 'living_room_bloc.dart'; + +sealed class LivingRoomEvent extends Equatable { + const LivingRoomEvent(); + + @override + List get props => []; +} + +class LivingRoomFetchDeviceStatusEvent extends LivingRoomEvent { + final String deviceId; + + const LivingRoomFetchDeviceStatusEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +//LivingRoomFetchBatchStatus +class LivingRoomFetchBatchEvent extends LivingRoomEvent { + final List devicesIds; + + const LivingRoomFetchBatchEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class LivingRoomControl extends LivingRoomEvent { + final String deviceId; + final String code; + final bool value; + + const LivingRoomControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class LivingRoomBatchControl extends LivingRoomEvent { + final List devicesIds; + final String code; + final bool value; + + const LivingRoomBatchControl( + {required this.devicesIds, required this.code, required this.value}); + + @override + List get props => [devicesIds, code, value]; +} + +class LivingRoomFactoryResetEvent extends LivingRoomEvent { + final String uuid; + final FactoryResetModel factoryReset; + const LivingRoomFactoryResetEvent(this.uuid, this.factoryReset); + + @override + List get props => [uuid, factoryReset]; +} diff --git a/lib/pages/device_managment/living_room_switch/bloc/living_room_state.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_state.dart similarity index 100% rename from lib/pages/device_managment/living_room_switch/bloc/living_room_state.dart rename to lib/pages/device_managment/three_gang_switch/bloc/living_room_state.dart diff --git a/lib/pages/device_managment/living_room_switch/helper/living_room_helper.dart b/lib/pages/device_managment/three_gang_switch/helper/living_room_helper.dart similarity index 65% rename from lib/pages/device_managment/living_room_switch/helper/living_room_helper.dart rename to lib/pages/device_managment/three_gang_switch/helper/living_room_helper.dart index 8e3f4985..255204b2 100644 --- a/lib/pages/device_managment/living_room_switch/helper/living_room_helper.dart +++ b/lib/pages/device_managment/three_gang_switch/helper/living_room_helper.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/widgets/cieling_light.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/widgets/spot_light.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/widgets/wall_light.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/widgets/cieling_light.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/widgets/spot_light.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/widgets/wall_light.dart'; mixin LivingRoomHelper { Widget livingRoomControlWidgets( @@ -18,4 +18,3 @@ mixin LivingRoomHelper { } } } - diff --git a/lib/pages/device_managment/living_room_switch/models/living_room_model.dart b/lib/pages/device_managment/three_gang_switch/models/living_room_model.dart similarity index 100% rename from lib/pages/device_managment/living_room_switch/models/living_room_model.dart rename to lib/pages/device_managment/three_gang_switch/models/living_room_model.dart diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart new file mode 100644 index 00000000..0d82c515 --- /dev/null +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class LivingRoomBatchControlsView extends StatelessWidget + with HelperResponsiveLayout { + const LivingRoomBatchControlsView({super.key, required this.deviceIds}); + + final List deviceIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LivingRoomBloc(deviceId: deviceIds.first) + ..add(LivingRoomFetchBatchEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is LivingRoomDeviceStatusLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is LivingRoomDeviceStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is LivingRoomDeviceManagementError || + state is LivingRoomControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, LivingRoomStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return SizedBox( + child: GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Wall Light', + onChange: (value) { + context.read().add( + LivingRoomBatchControl( + devicesIds: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceIds.first, + label: 'Ceiling Light', + onChange: (value) { + context.read().add( + LivingRoomBatchControl( + devicesIds: deviceIds, + code: 'switch_2', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch3, + code: 'switch_2', + deviceId: deviceIds.first, + label: 'Spotlight', + onChange: (value) { + context.read().add( + LivingRoomBatchControl( + devicesIds: deviceIds, + code: 'switch_3', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget(callFactoryReset: () { + context.read().add( + LivingRoomFactoryResetEvent( + status.uuid, + FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/living_room_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart similarity index 61% rename from lib/pages/device_managment/living_room_switch/view/living_room_device_control.dart rename to lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 60e656ad..b7f97776 100644 --- a/lib/pages/device_managment/living_room_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,21 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/models/living_room_model.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/widgets/living_toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class LivingRoomDeviceControl extends StatelessWidget +class LivingRoomDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { final String deviceId; - const LivingRoomDeviceControl({super.key, required this.deviceId}); + const LivingRoomDeviceControlsView({super.key, required this.deviceId}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => LivingRoomBloc(deviceId: deviceId) - ..add(LivingRoomFetchDeviceStatus(deviceId)), + ..add(LivingRoomFetchDeviceStatusEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is LivingRoomDeviceStatusLoading) { @@ -24,7 +24,7 @@ class LivingRoomDeviceControl extends StatelessWidget return _buildStatusControls(context, state.status); } else if (state is LivingRoomDeviceManagementError || state is LivingRoomControlError) { - return Center(child: Text(state.toString())); + return const Center(child: Text('Error fetching status')); } else { return const Center(child: CircularProgressIndicator()); } @@ -35,6 +35,7 @@ class LivingRoomDeviceControl extends StatelessWidget Widget _buildStatusControls( BuildContext context, LivingRoomStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return GridView( @@ -42,7 +43,7 @@ class LivingRoomDeviceControl extends StatelessWidget shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isLarge + crossAxisCount: isLarge || isExtraLarge ? 3 : isMedium ? 2 @@ -57,18 +58,36 @@ class LivingRoomDeviceControl extends StatelessWidget code: 'switch_1', deviceId: deviceId, label: 'Wall Light', + onChange: (value) { + context.read().add( + LivingRoomControl( + deviceId: deviceId, code: 'switch_1', value: value), + ); + }, ), ToggleWidget( value: status.switch2, code: 'switch_2', deviceId: deviceId, label: 'Ceiling Light', + onChange: (value) { + context.read().add( + LivingRoomControl( + deviceId: deviceId, code: 'switch_2', value: value), + ); + }, ), ToggleWidget( value: status.switch3, code: 'switch_3', deviceId: deviceId, label: 'Spotlight', + onChange: (value) { + context.read().add( + LivingRoomControl( + deviceId: deviceId, code: 'switch_3', value: value), + ); + }, ), ], ); diff --git a/lib/pages/device_managment/living_room_switch/widgets/cieling_light.dart b/lib/pages/device_managment/three_gang_switch/widgets/cieling_light.dart similarity index 89% rename from lib/pages/device_managment/living_room_switch/widgets/cieling_light.dart rename to lib/pages/device_managment/three_gang_switch/widgets/cieling_light.dart index b529e6e6..629c131b 100644 --- a/lib/pages/device_managment/living_room_switch/widgets/cieling_light.dart +++ b/lib/pages/device_managment/three_gang_switch/widgets/cieling_light.dart @@ -2,16 +2,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class CeilingLight extends StatelessWidget { - const CeilingLight( - {super.key, - required this.value, - required this.code, - required this.deviceId}); + const CeilingLight({super.key, required this.value, required this.code, required this.deviceId}); final bool value; final String code; diff --git a/lib/pages/device_managment/living_room_switch/widgets/spot_light.dart b/lib/pages/device_managment/three_gang_switch/widgets/spot_light.dart similarity index 89% rename from lib/pages/device_managment/living_room_switch/widgets/spot_light.dart rename to lib/pages/device_managment/three_gang_switch/widgets/spot_light.dart index 14c4fe5e..6ac71a38 100644 --- a/lib/pages/device_managment/living_room_switch/widgets/spot_light.dart +++ b/lib/pages/device_managment/three_gang_switch/widgets/spot_light.dart @@ -2,16 +2,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class SpotLight extends StatelessWidget { - const SpotLight( - {super.key, - required this.value, - required this.code, - required this.deviceId}); + const SpotLight({super.key, required this.value, required this.code, required this.deviceId}); final bool value; final String code; diff --git a/lib/pages/device_managment/living_room_switch/widgets/wall_light.dart b/lib/pages/device_managment/three_gang_switch/widgets/wall_light.dart similarity index 89% rename from lib/pages/device_managment/living_room_switch/widgets/wall_light.dart rename to lib/pages/device_managment/three_gang_switch/widgets/wall_light.dart index 8e168ec0..12c814ac 100644 --- a/lib/pages/device_managment/living_room_switch/widgets/wall_light.dart +++ b/lib/pages/device_managment/three_gang_switch/widgets/wall_light.dart @@ -2,16 +2,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:syncrow_web/pages/device_managment/living_room_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class WallLight extends StatelessWidget { - const WallLight( - {super.key, - required this.value, - required this.code, - required this.deviceId}); + const WallLight({super.key, required this.value, required this.code, required this.deviceId}); final bool value; final String code; diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart new file mode 100644 index 00000000..5169b0e4 --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart @@ -0,0 +1,181 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'two_gang_glass_switch_event.dart'; +part 'two_gang_glass_switch_state.dart'; + +class TwoGangGlassSwitchBloc + extends Bloc { + TwoGangGlassStatusModel deviceStatus; + Timer? _timer; + + TwoGangGlassSwitchBloc({required String deviceId}) + : deviceStatus = TwoGangGlassStatusModel( + uuid: deviceId, + switch1: false, + countDown1: 0, + switch2: false, + countDown2: 0), + super(TwoGangGlassSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFactoryReset); + } + + Future _onFetchDeviceStatus(TwoGangGlassSwitchFetchDeviceEvent event, + Emitter emit) async { + emit(TwoGangGlassSwitchLoading()); + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = + TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onControl(TwoGangGlassSwitchControl event, + Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _onBatchControl(TwoGangGlassSwitchBatchControl event, + Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + Future _onFetchBatchStatus( + TwoGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit) async { + emit(TwoGangGlassSwitchLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = TwoGangGlassStatusModel.fromJson( + event.deviceIds.first, status.status); + emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onFactoryReset(TwoGangGlassFactoryReset event, + Emitter emit) async { + emit(TwoGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi() + .factoryReset(event.factoryReset, event.deviceId); + if (!response) { + emit(TwoGangGlassSwitchError('Failed')); + } else { + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _revertValueAndEmit(String deviceId, String code, bool oldValue, + Emitter emit) { + _updateLocalValue(code, oldValue); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } else if (code == 'switch_2') { + deviceStatus = deviceStatus.copyWith(switch2: value); + } + } + + bool _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.switch1; + case 'switch_2': + return deviceStatus.switch2; + default: + return false; + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart new file mode 100644 index 00000000..f88d61fe --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart @@ -0,0 +1,50 @@ +part of 'two_gang_glass_switch_bloc.dart'; + +@immutable +abstract class TwoGangGlassSwitchEvent {} + +class TwoGangGlassSwitchFetchDeviceEvent extends TwoGangGlassSwitchEvent { + final String deviceId; + + TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); +} + +class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { + final String deviceId; + final String code; + final bool value; + + TwoGangGlassSwitchControl({ + required this.deviceId, + required this.code, + required this.value, + }); +} + +class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { + final List deviceIds; + final String code; + final bool value; + + TwoGangGlassSwitchBatchControl({ + required this.deviceIds, + required this.code, + required this.value, + }); +} + +class TwoGangGlassSwitchFetchBatchStatusEvent extends TwoGangGlassSwitchEvent { + final List deviceIds; + + TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); +} + +class TwoGangGlassFactoryReset extends TwoGangGlassSwitchEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + TwoGangGlassFactoryReset({ + required this.deviceId, + required this.factoryReset, + }); +} diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_state.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_state.dart new file mode 100644 index 00000000..3f95a514 --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_state.dart @@ -0,0 +1,32 @@ +part of 'two_gang_glass_switch_bloc.dart'; + +@immutable +abstract class TwoGangGlassSwitchState {} + +class TwoGangGlassSwitchInitial extends TwoGangGlassSwitchState {} + +class TwoGangGlassSwitchLoading extends TwoGangGlassSwitchState {} + +class TwoGangGlassSwitchStatusLoaded extends TwoGangGlassSwitchState { + final TwoGangGlassStatusModel status; + + TwoGangGlassSwitchStatusLoaded(this.status); +} + +class TwoGangGlassSwitchError extends TwoGangGlassSwitchState { + final String message; + + TwoGangGlassSwitchError(this.message); +} + +class TwoGangGlassSwitchBatchStatusLoaded extends TwoGangGlassSwitchState { + final TwoGangGlassStatusModel status; + + TwoGangGlassSwitchBatchStatusLoaded(this.status); +} + +class TwoGangGlassSwitchBatchControlError extends TwoGangGlassSwitchState { + final String message; + + TwoGangGlassSwitchBatchControlError(this.message); +} diff --git a/lib/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart b/lib/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart new file mode 100644 index 00000000..54d99d74 --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart @@ -0,0 +1,69 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class TwoGangGlassStatusModel { + final String uuid; + final bool switch1; + final int countDown1; + final bool switch2; + final int countDown2; + + TwoGangGlassStatusModel({ + required this.uuid, + required this.switch1, + required this.countDown1, + required this.switch2, + required this.countDown2, + }); + + factory TwoGangGlassStatusModel.fromJson(String id, List jsonList) { + late bool switch1; + late int countDown1; + late bool switch2; + late int countDown2; + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + switch1 = status.value ?? false; + break; + case 'countdown_1': + countDown1 = status.value ?? 0; + break; + case 'switch_2': + switch2 = status.value ?? false; + break; + case 'countdown_2': + countDown2 = status.value ?? 0; + break; + } + } + + return TwoGangGlassStatusModel( + uuid: id, + switch1: switch1, + countDown1: countDown1, + switch2: switch2, + countDown2: countDown2, + ); + } + + TwoGangGlassStatusModel copyWith({ + String? uuid, + bool? switch1, + int? countDown1, + bool? switch2, + int? countDown2, + }) { + return TwoGangGlassStatusModel( + uuid: uuid ?? this.uuid, + switch1: switch1 ?? this.switch1, + countDown1: countDown1 ?? this.countDown1, + switch2: switch2 ?? this.switch2, + countDown2: countDown2 ?? this.countDown2, + ); + } + + @override + String toString() => + 'TwoGangGlassStatusModel(uuid: $uuid, switch1: $switch1, countDown1: $countDown1, switch2: $switch2, countDown2: $countDown2)'; +} diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart new file mode 100644 index 00000000..d0288ca3 --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class TwoGangGlassSwitchBatchControlView extends StatelessWidget with HelperResponsiveLayout { + final List deviceIds; + + const TwoGangGlassSwitchBatchControlView({required this.deviceIds, super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + TwoGangGlassSwitchBloc(deviceId: deviceIds.first)..add(TwoGangGlassSwitchFetchBatchStatusEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is TwoGangGlassSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is TwoGangGlassSwitchBatchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is TwoGangGlassSwitchError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, TwoGangGlassStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Wall Light', + onChange: (value) { + context.read().add( + TwoGangGlassSwitchBatchControl( + deviceIds: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceIds.first, + label: 'Ceiling Light', + onChange: (value) { + context.read().add( + TwoGangGlassSwitchBatchControl( + deviceIds: deviceIds, + code: 'switch_2', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, // adjust the version according to your requirement + ), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + TwoGangGlassFactoryReset( + deviceId: status.uuid, + factoryReset: FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart new file mode 100644 index 00000000..72f69763 --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class TwoGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + + const TwoGangGlassSwitchControlView({required this.deviceId, super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + TwoGangGlassSwitchBloc(deviceId: deviceId)..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is TwoGangGlassSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is TwoGangGlassSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is TwoGangGlassSwitchError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, TwoGangGlassStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: 'Wall Light', + onChange: (value) { + context.read().add( + TwoGangGlassSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceId, + label: 'Ceiling Light', + onChange: (value) { + context.read().add( + TwoGangGlassSwitchControl( + deviceId: deviceId, + code: 'switch_2', + value: value, + ), + ); + }, + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Preferences', + icon: Assets.preferences, + onChange: (value) {}, + showToggle: false, + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Scheduling', + icon: Assets.scheduling, + onChange: (value) {}, + showToggle: false, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart new file mode 100644 index 00000000..0d35d8e8 --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart @@ -0,0 +1,177 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class TwoGangSwitchBloc extends Bloc { + TwoGangSwitchBloc({required this.deviceId}) : super(TwoGangSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onFetchBatchStatus); + on(_onBatchControl); + on(_onFactoryReset); + } + + late TwoGangStatusModel deviceStatus; + final String deviceId; + Timer? _timer; + + FutureOr _onFetchDeviceStatus(TwoGangSwitchFetchDeviceEvent event, + Emitter emit) async { + emit(TwoGangSwitchLoading()); + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + + deviceStatus = TwoGangStatusModel.fromJson(event.deviceId, status.status); + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangSwitchError(e.toString())); + } + } + + FutureOr _onControl( + TwoGangSwitchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required bool value, + required bool oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _revertValueAndEmit(String deviceId, String code, bool oldValue, + Emitter emit) { + _updateLocalValue(code, oldValue); + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + + if (code == 'switch_2') { + deviceStatus = deviceStatus.copyWith(switch2: value); + } + } + + bool _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.switch1; + case 'switch_2': + return deviceStatus.switch2; + default: + return false; + } + } + + Future _onFetchBatchStatus(TwoGangSwitchFetchBatchEvent event, + Emitter emit) async { + emit(TwoGangSwitchLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + TwoGangStatusModel.fromJson(event.devicesIds.first, status.status); + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangSwitchError(e.toString())); + } + } + + @override + Future close() { + _timer?.cancel(); + return super.close(); + } + + FutureOr _onBatchControl( + TwoGangSwitchBatchControl event, Emitter emit) async { + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + FutureOr _onFactoryReset( + TwoGangFactoryReset event, Emitter emit) async { + emit(TwoGangSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(TwoGangSwitchError('Failed')); + } else { + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(TwoGangSwitchError(e.toString())); + } + } +} diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart new file mode 100644 index 00000000..16973b3a --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +class TwoGangSwitchEvent extends Equatable { + @override + List get props => []; +} + +class TwoGangSwitchFetchDeviceEvent extends TwoGangSwitchEvent { + final String deviceId; + + TwoGangSwitchFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class TwoGangSwitchControl extends TwoGangSwitchEvent { + final String deviceId; + final String code; + final bool value; + + TwoGangSwitchControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class TwoGangSwitchFetchBatchEvent extends TwoGangSwitchEvent { + final List devicesIds; + + TwoGangSwitchFetchBatchEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class TwoGangSwitchBatchControl extends TwoGangSwitchEvent { + final List deviceId; + final String code; + final bool value; + + TwoGangSwitchBatchControl( + {required this.deviceId, required this.code, required this.value}); + + @override + List get props => [deviceId, code, value]; +} + +class TwoGangFactoryReset extends TwoGangSwitchEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + TwoGangFactoryReset({required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart new file mode 100644 index 00000000..b9208211 --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; + +class TwoGangSwitchState extends Equatable { + @override + List get props => []; +} + +class TwoGangSwitchInitial extends TwoGangSwitchState {} + +class TwoGangSwitchLoading extends TwoGangSwitchState {} + +class TwoGangSwitchStatusLoaded extends TwoGangSwitchState { + final TwoGangStatusModel status; + + TwoGangSwitchStatusLoaded(this.status); + + @override + List get props => [status]; +} + +class TwoGangSwitchError extends TwoGangSwitchState { + final String message; + + TwoGangSwitchError(this.message); + + @override + List get props => [message]; +} + +class TwoGangSwitchControlError extends TwoGangSwitchState { + final String message; + + TwoGangSwitchControlError(this.message); + + @override + List get props => [message]; +} + +class TwoGangSwitchBatchControlError extends TwoGangSwitchState { + final String message; + + TwoGangSwitchBatchControlError(this.message); + + @override + List get props => [message]; +} + +class TwoGangSwitchBatchStatusLoaded extends TwoGangSwitchState { + final List status; + + TwoGangSwitchBatchStatusLoaded(this.status); + + @override + List get props => [status]; +} diff --git a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart new file mode 100644 index 00000000..6cec4256 --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart @@ -0,0 +1,65 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class TwoGangStatusModel { + final String uuid; + final bool switch1; + final bool switch2; + final int countDown; + final int countDown2; + + TwoGangStatusModel({ + required this.uuid, + required this.switch1, + required this.switch2, + required this.countDown, + required this.countDown2, + }); + + factory TwoGangStatusModel.fromJson(String id, List jsonList) { + late bool switch1; + late bool switch2; + late int countDown; + late int countDown2; + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + switch1 = status.value ?? false; + break; + case 'countdown_1': + countDown = status.value ?? 0; + break; + case 'switch_2': + switch2 = status.value ?? false; + break; + case 'countdown_2': + countDown2 = status.value ?? 0; + break; + } + } + + return TwoGangStatusModel( + uuid: id, + switch1: switch1, + countDown: countDown, + switch2: switch2, + countDown2: countDown2, + ); + } + + TwoGangStatusModel copyWith({ + String? uuid, + bool? switch1, + int? countDown, + bool? switch2, + int? countDown2, + }) { + return TwoGangStatusModel( + uuid: uuid ?? this.uuid, + switch1: switch1 ?? this.switch1, + countDown: countDown ?? this.countDown, + switch2: switch2 ?? this.switch2, + countDown2: countDown2 ?? this.countDown2, + ); + } +} diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart new file mode 100644 index 00000000..52900155 --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class TwoGangBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + const TwoGangBatchControlView({super.key, required this.deviceIds}); + + final List deviceIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => TwoGangSwitchBloc(deviceId: deviceIds.first) + ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is TwoGangSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is TwoGangSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is TwoGangSwitchError || + state is TwoGangSwitchControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, TwoGangStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return SizedBox( + child: GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Wall Light', + onChange: (value) { + context.read().add(TwoGangSwitchBatchControl( + deviceId: deviceIds, + code: 'switch_1', + value: value, + )); + }, + ), + ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceIds.first, + label: 'Ceiling Light', + onChange: (value) { + context.read().add(TwoGangSwitchBatchControl( + deviceId: deviceIds, + code: 'switch_2', + value: value, + )); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget(callFactoryReset: () { + context.read().add( + TwoGangFactoryReset( + deviceId: status.uuid, + factoryReset: FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart new file mode 100644 index 00000000..840d356e --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class TwoGangDeviceControlView extends StatelessWidget + with HelperResponsiveLayout { + final String deviceId; + + const TwoGangDeviceControlView({super.key, required this.deviceId}); + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => TwoGangSwitchBloc(deviceId: deviceId) + ..add(TwoGangSwitchFetchDeviceEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is TwoGangSwitchLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is TwoGangSwitchStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is TwoGangSwitchError || + state is TwoGangSwitchControlError) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context, TwoGangStatusModel status) { + return Center( + child: Wrap( + alignment: WrapAlignment.center, + spacing: 12, + runSpacing: 12, + children: [ + SizedBox( + width: 200, + child: ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: 'Wall Light', + onChange: (value) { + context.read().add(TwoGangSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, + )); + }, + ), + ), + SizedBox( + width: 200, + child: ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceId, + label: 'Ceiling Light', + onChange: (value) { + context.read().add(TwoGangSwitchControl( + deviceId: deviceId, + code: 'switch_2', + value: value, + )); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/wall_sensor/bloc/bloc.dart b/lib/pages/device_managment/wall_sensor/bloc/bloc.dart deleted file mode 100644 index a2697cd3..00000000 --- a/lib/pages/device_managment/wall_sensor/bloc/bloc.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'dart:async'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/event.dart'; -import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/state.dart'; -import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; -import 'package:syncrow_web/services/devices_mang_api.dart'; - -class WallSensorBloc extends Bloc { - final String deviceId; - late WallSensorModel deviceStatus; - Timer? _timer; - - WallSensorBloc({required this.deviceId}) : super(WallSensorInitialState()) { - on(_fetchWallSensorStatus); - on(_changeValue); - on(_getDeviceReports); - on(_showDescription); - on(_backToGridView); - } - - void _fetchWallSensorStatus( - WallSensorInitialEvent event, Emitter emit) async { - emit(WallSensorLoadingInitialState()); - try { - var response = await DevicesManagementApi().getDeviceStatus(deviceId); - deviceStatus = WallSensorModel.fromJson(response.status); - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - // _listenToChanges(); - } catch (e) { - emit(WallSensorFailedState(error: e.toString())); - return; - } - } - - // _listenToChanges() { - // try { - // DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - // Stream stream = ref.onValue; - - // stream.listen((DatabaseEvent event) { - // Map usersMap = event.snapshot.value as Map; - // List statusList = []; - - // usersMap['status'].forEach((element) { - // statusList.add(StatusModel(code: element['code'], value: element['value'])); - // }); - - // deviceStatus = WallSensorModel.fromJson(statusList); - // add(WallSensorUpdatedEvent()); - // }); - // } catch (_) {} - // } - - void _changeValue( - WallSensorChangeValueEvent event, Emitter emit) async { - emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, code: event.code, value: event.value); - } - - _runDeBouncer({ - required String deviceId, - required String code, - required dynamic value, - }) { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - - if (!response) { - add(WallSensorInitialEvent()); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(WallSensorInitialEvent()); - } - }); - } - - FutureOr _getDeviceReports( - GetDeviceReportsEvent event, Emitter emit) async { - emit(DeviceReportsLoadingState()); - - try { - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(DeviceReportsState(deviceReport: value)); - }); - } catch (e) { - emit(DeviceReportsFailedState(error: e.toString())); - return; - } - } - - void _showDescription( - ShowDescriptionEvent event, Emitter emit) { - emit(WallSensorShowDescriptionState(description: event.description)); - } - - void _backToGridView( - BackToGridViewEvent event, Emitter emit) { - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - } -} diff --git a/lib/pages/device_managment/wall_sensor/bloc/event.dart b/lib/pages/device_managment/wall_sensor/bloc/event.dart deleted file mode 100644 index d3c20ba7..00000000 --- a/lib/pages/device_managment/wall_sensor/bloc/event.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:equatable/equatable.dart'; - -abstract class WallSensorEvent extends Equatable { - const WallSensorEvent(); - - @override - List get props => []; -} - -class WallSensorInitialEvent extends WallSensorEvent {} - -class WallSensorChangeValueEvent extends WallSensorEvent { - final int value; - final String code; - const WallSensorChangeValueEvent({required this.value, required this.code}); - - @override - List get props => [value, code]; -} - -class GetDeviceReportsEvent extends WallSensorEvent { - final String deviceUuid; - final String code; - const GetDeviceReportsEvent({ - required this.deviceUuid, - required this.code, - }); - - @override - List get props => [deviceUuid, code]; -} - -class ShowDescriptionEvent extends WallSensorEvent { - final String description; - const ShowDescriptionEvent({required this.description}); -} - -class BackToGridViewEvent extends WallSensorEvent {} diff --git a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart new file mode 100644 index 00000000..41598439 --- /dev/null +++ b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart @@ -0,0 +1,187 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class WallSensorBloc extends Bloc { + final String deviceId; + late WallSensorModel deviceStatus; + Timer? _timer; + + WallSensorBloc({required this.deviceId}) : super(WallSensorInitialState()) { + on(_fetchWallSensorStatus); + on(_fetchWallSensorBatchControl); + on(_changeValue); + on(_onBatchControl); + on(_getDeviceReports); + on(_showDescription); + on(_backToGridView); + on(_onFactoryReset); + } + + void _fetchWallSensorStatus( + WallSensorFetchStatusEvent event, Emitter emit) async { + emit(WallSensorLoadingInitialState()); + try { + var response = await DevicesManagementApi().getDeviceStatus(deviceId); + deviceStatus = WallSensorModel.fromJson(response.status); + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + // _listenToChanges(); + } catch (e) { + emit(WallSensorFailedState(error: e.toString())); + return; + } + } + + // Fetch batch status + FutureOr _fetchWallSensorBatchControl( + WallSensorFetchBatchStatusEvent event, Emitter emit) async { + emit(WallSensorLoadingInitialState()); + try { + var response = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = WallSensorModel.fromJson(response.status); + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + } catch (e) { + emit(WallSensorFailedState(error: e.toString())); + } + } + + // _listenToChanges() { + // try { + // DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + // Stream stream = ref.onValue; + + // stream.listen((DatabaseEvent event) { + // Map usersMap = event.snapshot.value as Map; + // List statusList = []; + + // usersMap['status'].forEach((element) { + // statusList.add(StatusModel(code: element['code'], value: element['value'])); + // }); + + // deviceStatus = WallSensorModel.fromJson(statusList); + // add(WallSensorUpdatedEvent()); + // }); + // } catch (_) {} + // } + + void _changeValue(WallSensorChangeValueEvent event, Emitter emit) async { + emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); + if (event.code == 'far_detection') { + deviceStatus.farDetection = event.value; + } else if (event.code == 'motionless_sensitivity') { + deviceStatus.motionlessSensitivity = event.value; + } else if (event.code == 'motion_sensitivity_value') { + deviceStatus.motionSensitivity = event.value; + } else if (event.code == 'no_one_time') { + deviceStatus.noBodyTime = event.value; + } + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + await _runDeBouncer( + deviceId: deviceId, + code: event.code, + value: event.value, + isBatch: false, + emit: emit, + ); + } + + Future _onBatchControl( + WallSensorBatchControlEvent event, Emitter emit) async { + emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); + if (event.code == 'far_detection') { + deviceStatus.farDetection = event.value; + } else if (event.code == 'motionless_sensitivity') { + deviceStatus.motionlessSensitivity = event.value; + } else if (event.code == 'motion_sensitivity_value') { + deviceStatus.motionSensitivity = event.value; + } else if (event.code == 'no_one_time') { + deviceStatus.noBodyTime = event.value; + } + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + await _runDeBouncer( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + emit: emit, + isBatch: true, + ); + } + + _runDeBouncer({ + required dynamic deviceId, + required String code, + required dynamic value, + required Emitter emit, + required bool isBatch, + }) { + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(const Duration(seconds: 1), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi().deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + add(WallSensorFetchStatusEvent()); + } + } catch (_) { + await Future.delayed(const Duration(milliseconds: 500)); + add(WallSensorFetchStatusEvent()); + } + }); + } + + FutureOr _getDeviceReports( + GetDeviceReportsEvent event, Emitter emit) async { + emit(DeviceReportsLoadingState()); + // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; + // final to = DateTime.now().millisecondsSinceEpoch; + + try { + // await DevicesManagementApi.getDeviceReportsByDate( + // deviceId, event.code, from.toString(), to.toString()) + await DevicesManagementApi.getDeviceReports(deviceId, event.code).then((value) { + emit(DeviceReportsState(deviceReport: value, code: event.code)); + }); + } catch (e) { + emit(DeviceReportsFailedState(error: e.toString())); + return; + } + } + + void _showDescription(ShowDescriptionEvent event, Emitter emit) { + emit(WallSensorShowDescriptionState(description: event.description)); + } + + void _backToGridView(BackToGridViewEvent event, Emitter emit) { + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + } + + FutureOr _onFactoryReset( + WallSensorFactoryResetEvent event, Emitter emit) async { + emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const WallSensorFailedState(error: 'Failed')); + } else { + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + } + } catch (e) { + emit(WallSensorFailedState(error: e.toString())); + } + } +} diff --git a/lib/pages/device_managment/wall_sensor/bloc/wall_event.dart b/lib/pages/device_managment/wall_sensor/bloc/wall_event.dart new file mode 100644 index 00000000..17d85d43 --- /dev/null +++ b/lib/pages/device_managment/wall_sensor/bloc/wall_event.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +abstract class WallSensorEvent extends Equatable { + const WallSensorEvent(); + + @override + List get props => []; +} + +class WallSensorFetchStatusEvent extends WallSensorEvent {} + +class WallSensorChangeValueEvent extends WallSensorEvent { + final int value; + final String code; + const WallSensorChangeValueEvent({required this.value, required this.code}); + + @override + List get props => [value, code]; +} + +class WallSensorFetchBatchStatusEvent extends WallSensorEvent { + final List devicesIds; + const WallSensorFetchBatchStatusEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class GetDeviceReportsEvent extends WallSensorEvent { + final String deviceUuid; + final String code; + const GetDeviceReportsEvent({ + required this.deviceUuid, + required this.code, + }); + + @override + List get props => [deviceUuid, code]; +} + +class ShowDescriptionEvent extends WallSensorEvent { + final String description; + const ShowDescriptionEvent({required this.description}); +} + +class BackToGridViewEvent extends WallSensorEvent {} + +class WallSensorBatchControlEvent extends WallSensorEvent { + final List deviceIds; + final String code; + final dynamic value; + + const WallSensorBatchControlEvent({ + required this.deviceIds, + required this.code, + required this.value, + }); + + @override + List get props => [deviceIds, code, value]; +} + +class WallSensorFactoryResetEvent extends WallSensorEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const WallSensorFactoryResetEvent({ + required this.deviceId, + required this.factoryReset, + }); +} diff --git a/lib/pages/device_managment/wall_sensor/bloc/state.dart b/lib/pages/device_managment/wall_sensor/bloc/wall_state.dart similarity index 94% rename from lib/pages/device_managment/wall_sensor/bloc/state.dart rename to lib/pages/device_managment/wall_sensor/bloc/wall_state.dart index 67ce0d19..19a154a9 100644 --- a/lib/pages/device_managment/wall_sensor/bloc/state.dart +++ b/lib/pages/device_managment/wall_sensor/bloc/wall_state.dart @@ -42,7 +42,8 @@ class DeviceReportsLoadingState extends WallSensorState {} class DeviceReportsState extends WallSensorState { final DeviceReport deviceReport; - const DeviceReportsState({required this.deviceReport}); + final String code; + const DeviceReportsState({required this.deviceReport, required this.code}); } class DeviceReportsFailedState extends WallSensorState { diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart new file mode 100644 index 00000000..66ff67aa --- /dev/null +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WallSensorBatchControlView extends StatelessWidget with HelperResponsiveLayout { + const WallSensorBatchControlView({super.key, required this.devicesIds}); + + final List devicesIds; + + @override + Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return BlocProvider( + create: (context) => WallSensorBloc(deviceId: devicesIds.first) + ..add(WallSensorFetchBatchStatusEvent(devicesIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is WallSensorLoadingInitialState || state is DeviceReportsLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WallSensorUpdateState) { + return _buildGridView(context, state.wallSensorModel, isExtraLarge, isLarge, isMedium); + } else if (state is DeviceReportsFailedState) { + final model = context.read().deviceStatus; + return _buildGridView(context, model, isExtraLarge, isLarge, isMedium); + } + return const Center(child: Text('Error fetching status')); + }, + ), + ); + } + + Widget _buildGridView( + BuildContext context, WallSensorModel model, bool isExtraLarge, bool isLarge, bool isMedium) { + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + PresenceUpdateData( + value: model.motionSensitivity.toDouble(), + title: 'Motion Detection Sensitivity:', + minValue: 1, + maxValue: 5, + steps: 1, + action: (int value) { + context.read().add( + WallSensorBatchControlEvent( + deviceIds: devicesIds, + code: 'motion_sensitivity_value', + value: value, + ), + ); + }, + ), + PresenceUpdateData( + value: model.motionlessSensitivity.toDouble(), + title: 'Motionless Detection Sensitivity:', + minValue: 1, + maxValue: 5, + steps: 1, + action: (int value) => context.read().add( + WallSensorBatchControlEvent( + deviceIds: devicesIds, + code: 'motionless_sensitivity', + value: value, + ), + ), + ), + PresenceUpdateData( + value: model.noBodyTime.toDouble(), + title: 'Nobody Time:', + minValue: 10, + maxValue: 10000, + steps: 1, + description: 'sec', + action: (int value) => context.read().add(WallSensorBatchControlEvent( + deviceIds: devicesIds, + code: 'no_one_time', + value: value, + ))), + PresenceUpdateData( + value: model.farDetection.toDouble(), + title: 'Far Detection:', + minValue: 75, + maxValue: 600, + steps: 75, + description: 'cm', + action: (int value) => context.read().add( + WallSensorBatchControlEvent( + deviceIds: devicesIds, + code: 'far_detection', + value: value, + ), + ), + ), + FirmwareUpdateWidget(deviceId: devicesIds.first, version: 2), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + WallSensorFactoryResetEvent( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart index 068cb27d..370edaa5 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/description_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; -import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/bloc.dart'; -import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/event.dart'; -import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/state.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_static_widget.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_status.dart'; @@ -14,29 +14,30 @@ import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class WallSensorControls extends StatelessWidget with HelperResponsiveLayout { - const WallSensorControls({super.key, required this.device}); +class WallSensorControlsView extends StatelessWidget with HelperResponsiveLayout { + const WallSensorControlsView({super.key, required this.device}); final AllDevicesModel device; @override Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( create: (context) => - WallSensorBloc(deviceId: device.uuid!)..add(WallSensorInitialEvent()), + WallSensorBloc(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), child: BlocBuilder( builder: (context, state) { - if (state is WallSensorLoadingInitialState || - state is DeviceReportsLoadingState) { + if (state is WallSensorLoadingInitialState || state is DeviceReportsLoadingState) { return const Center(child: CircularProgressIndicator()); } else if (state is WallSensorUpdateState) { - return _buildGridView( - context, state.wallSensorModel, isLarge, isMedium); + return _buildGridView(context, state.wallSensorModel, isExtraLarge, isLarge, isMedium); } else if (state is DeviceReportsState) { return ReportsTable( report: state.deviceReport, + thirdColumnTitle: state.code == 'illuminance_value' ? "Value" : 'Status', + thirdColumnDescription: state.code == 'illuminance_value' ? "Lux" : null, onRowTap: (index) {}, onClose: () { context.read().add(BackToGridViewEvent()); @@ -51,7 +52,7 @@ class WallSensorControls extends StatelessWidget with HelperResponsiveLayout { ); } else if (state is DeviceReportsFailedState) { final model = context.read().deviceStatus; - return _buildGridView(context, model, isLarge, isMedium); + return _buildGridView(context, model, isExtraLarge, isLarge, isMedium); } return const Center(child: Text('Error fetching status')); }, @@ -59,14 +60,14 @@ class WallSensorControls extends StatelessWidget with HelperResponsiveLayout { ); } - Widget _buildGridView(BuildContext context, WallSensorModel model, - bool isLarge, bool isMedium) { + Widget _buildGridView( + BuildContext context, WallSensorModel model, bool isExtraLarge, bool isLarge, bool isMedium) { return GridView( padding: const EdgeInsets.symmetric(horizontal: 50), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isLarge + crossAxisCount: isLarge || isExtraLarge ? 3 : isMedium ? 2 @@ -128,12 +129,11 @@ class WallSensorControls extends StatelessWidget with HelperResponsiveLayout { minValue: 10, maxValue: 10000, steps: 1, - description: 'hr', - action: (int value) => - context.read().add(WallSensorChangeValueEvent( - code: 'no_one_time', - value: value, - ))), + description: 'sec', + action: (int value) => context.read().add(WallSensorChangeValueEvent( + code: 'no_one_time', + value: value, + ))), PresenceUpdateData( value: model.farDetection.toDouble(), title: 'Far Detection:', @@ -150,8 +150,9 @@ class WallSensorControls extends StatelessWidget with HelperResponsiveLayout { ), GestureDetector( onTap: () { - context.read().add(GetDeviceReportsEvent( - code: 'illuminance_value', deviceUuid: device.uuid!)); + context + .read() + .add(GetDeviceReportsEvent(code: 'illuminance_value', deviceUuid: device.uuid!)); }, child: const PresenceStaticWidget( icon: Assets.illuminanceRecordIcon, @@ -160,8 +161,9 @@ class WallSensorControls extends StatelessWidget with HelperResponsiveLayout { ), GestureDetector( onTap: () { - context.read().add(GetDeviceReportsEvent( - code: 'presence_state', deviceUuid: device.uuid!)); + context + .read() + .add(GetDeviceReportsEvent(code: 'presence_state', deviceUuid: device.uuid!)); }, child: const PresenceStaticWidget( icon: Assets.presenceRecordIcon, diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart new file mode 100644 index 00000000..498c55fb --- /dev/null +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -0,0 +1,656 @@ +// water_heater_bloc.dart + +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; + +part 'water_heater_event.dart'; +part 'water_heater_state.dart'; + +class WaterHeaterBloc extends Bloc { + WaterHeaterBloc() : super(WaterHeaterInitial()) { + on(_fetchWaterHeaterStatus); + on(_controlWaterHeater); + on(_batchFetchWaterHeater); + on(_batchControlWaterHeater); + on(_updateScheduleEvent); + on(_stopScheduleEvent); + on(_onDecrementCountdown); + on(_initializeAddSchedule); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); + + on(_getSchedule); + on(_onAddSchedule); + on(_onEditSchedule); + on(_onDeleteSchedule); + on(_onUpdateSchedule); + } + + late WaterHeaterStatusModel deviceStatus; + Timer? _countdownTimer; + // Timer? _inchingTimer; + + FutureOr _initializeAddSchedule( + InitializeAddScheduleEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + if (event.isEditing) { + emit(currentState.copyWith( + selectedTime: event.selectedTime, + selectedDays: event.selectedDays ?? List.filled(7, false), + functionOn: event.functionOn ?? false, + isEditing: event.isEditing, + )); + } else { + emit(currentState.copyWith( + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + )); + } + } + + FutureOr _updateSelectedTime( + UpdateSelectedTimeEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + emit(currentState.copyWith(selectedTime: event.selectedTime)); + } + + FutureOr _updateSelectedDay( + UpdateSelectedDayEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final updatedDays = List.from(currentState.selectedDays); + updatedDays[event.index] = event.value; + emit(currentState.copyWith(selectedDays: updatedDays, selectedTime: currentState.selectedTime)); + } + + FutureOr _updateFunctionOn( + UpdateFunctionOnEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + emit(currentState.copyWith(functionOn: event.isOn, selectedTime: currentState.selectedTime)); + } + + FutureOr _updateScheduleEvent( + UpdateScheduleEvent event, + Emitter emit, + ) async { + final currentState = state; + if (currentState is WaterHeaterDeviceStatusLoaded) { + if (event.scheduleMode == ScheduleModes.schedule) { + emit(currentState.copyWith( + scheduleMode: ScheduleModes.schedule, + )); + } + if (event.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = Duration(hours: event.hours, minutes: event.minutes); + + emit(currentState.copyWith( + scheduleMode: ScheduleModes.countdown, + countdownHours: countdownRemaining.inHours, + countdownMinutes: countdownRemaining.inMinutes % 60, + isCountdownActive: currentState.isCountdownActive, + countdownRemaining: countdownRemaining, + )); + + if (!currentState.isCountdownActive! && countdownRemaining > Duration.zero) { + _startCountdownTimer(emit, countdownRemaining); + } + } else if (event.scheduleMode == ScheduleModes.inching) { + final inchingDuration = Duration(hours: event.hours, minutes: event.minutes); + + emit(currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + )); + } + } + } + + FutureOr _controlWaterHeater( + ToggleWaterHeaterEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(currentState.copyWith( + status: deviceStatus, + )); + + final success = await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + + if (success) { + if (event.code == "countdown_1") { + final countdownDuration = Duration(seconds: event.value); + + emit(currentState.copyWith( + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, + )); + + if (countdownDuration.inSeconds > 0) { + _startCountdownTimer(emit, countdownDuration); + } else { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + )); + } + } else if (event.code == "switch_inching") { + final inchingDuration = Duration(seconds: event.value); + //if (inchingDuration.inSeconds > 0) { + // _startInchingTimer(emit, inchingDuration); + // } else { + emit(currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + )); + // } + } + } + } + } + + FutureOr _stopScheduleEvent( + StopScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final isCountDown = currentState.scheduleMode == ScheduleModes.countdown; + + _countdownTimer?.cancel(); + + if (isCountDown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + )); + } else if (currentState.scheduleMode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + )); + } + + try { + final status = await DevicesManagementApi().deviceControl( + event.deviceId, + Status(code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), + ); + if (!status) { + emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); + } + } catch (e) { + emit(WaterHeaterFailedState(error: e.toString())); + } + } + } + + FutureOr _fetchWaterHeaterStatus( + WaterHeaterFetchStatusEvent event, + Emitter emit, + ) async { + emit(WaterHeaterLoadingState()); + + try { + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + + if (deviceStatus.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = Duration( + hours: deviceStatus.countdownHours, + minutes: deviceStatus.countdownMinutes, + ); + + if (countdownRemaining > Duration.zero) { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.countdown, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + isCountdownActive: true, + countdownRemaining: countdownRemaining, + )); + _startCountdownTimer(emit, countdownRemaining); + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.countdown, + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } + } else if (deviceStatus.scheduleMode == ScheduleModes.inching) { + final inchingDuration = Duration( + hours: deviceStatus.inchingHours, + minutes: deviceStatus.inchingMinutes, + ); + + if (inchingDuration > Duration.zero) { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.inching, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isInchingActive: true, + )); +//_startInchingTimer(emit, inchingDuration); + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.inching, + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + )); + } + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: deviceStatus.scheduleMode, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, + )); + } + } catch (e) { + emit(WaterHeaterFailedState(error: e.toString())); + } + } + + void _startCountdownTimer( + Emitter emit, + Duration countdownRemaining, + ) { + _countdownTimer?.cancel(); + + _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + add(DecrementCountdownEvent()); + }); + } + + // void _startInchingTimer( + // Emitter emit, + // Duration inchingDuration, + // ) { + // _inchingTimer?.cancel(); + + // _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + // add(DecrementInchingEvent()); + // }); + // } + + _onDecrementCountdown( + DecrementCountdownEvent event, + Emitter emit, + ) { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + if (currentState.countdownRemaining != null && currentState.countdownRemaining! > Duration.zero) { + final newRemaining = currentState.countdownRemaining! - const Duration(minutes: 1); + + if (newRemaining <= Duration.zero) { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + return; + } + + int totalSeconds = newRemaining.inSeconds; + + int newHours = totalSeconds ~/ 3600; + int newMinutes = (totalSeconds % 3600) ~/ 60; + + emit(currentState.copyWith( + countdownHours: newHours, + countdownMinutes: newMinutes, + countdownRemaining: newRemaining, + )); + } + } + } + + // FutureOr _onDecrementInching( + // DecrementInchingEvent event, + // Emitter emit, + // ) { + // if (state is WaterHeaterDeviceStatusLoaded) { + // final currentState = state as WaterHeaterDeviceStatusLoaded; + + // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) { + // final newRemaining = Duration( + // hours: currentState.inchingHours, + // minutes: currentState.inchingMinutes, + // ) - + // const Duration(minutes: 1); + + // if (newRemaining <= Duration.zero) { + // _inchingTimer?.cancel(); + // emit(currentState.copyWith( + // inchingHours: 0, + // inchingMinutes: 0, + // isInchingActive: false, + // )); + // } else { + // emit(currentState.copyWith( + // inchingHours: newRemaining.inHours, + // inchingMinutes: newRemaining.inMinutes % 60, + // )); + // } + // } + // } + // } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required dynamic value, + required dynamic oldValue, + required Emitter emit, + required bool isBatch, + }) async { + try { + late bool status; + await Future.delayed(const Duration(milliseconds: 500)); + + if (isBatch) { + status = await DevicesManagementApi().deviceBatchControl( + deviceId, + code, + value, + ); + } else { + status = await DevicesManagementApi().deviceControl( + deviceId, + Status(code: code, value: value), + ); + } + + if (!status) { + _revertValue(code, oldValue, emit.call); + return false; + } else { + return true; + } + } catch (e) { + _revertValue(code, oldValue, emit.call); + return false; + } + } + + void _revertValue(String code, dynamic oldValue, void Function(WaterHeaterState state) emit) { + _updateLocalValue(code, oldValue); + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + emit(currentState.copyWith( + status: deviceStatus, + )); + } + } + + void _updateLocalValue(String code, dynamic value) { + switch (code) { + case 'switch_1': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(heaterSwitch: value); + } + break; + case 'countdown_1': + if (value is int) { + deviceStatus = deviceStatus.copyWith( + countdownHours: value ~/ 60, + countdownMinutes: value % 60, + ); + } + break; + default: + break; + } + } + + dynamic _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.heaterSwitch; + case 'countdown_1': + return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes; + default: + return null; + } + } + + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + FutureOr _getSchedule(GetSchedulesEvent event, Emitter emit) async { + emit(ScheduleLoadingState()); + + try { + List schedules = + await DevicesManagementApi().getDeviceSchedules(deviceStatus.uuid, event.category); + + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: schedules, + scheduleMode: ScheduleModes.schedule, + )); + } catch (e) { + //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: const [], + )); + } + } + + FutureOr _onAddSchedule( + AddScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + ScheduleEntry newSchedule = ScheduleEntry( + category: event.category, + time: formatTimeOfDayToISO(event.time), + function: Status(code: 'switch_1', value: event.functionOn), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), + ); + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi().addScheduleRecord(newSchedule, currentState.status.uuid); + + if (success) { + add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); + } else { + emit(currentState); + //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); + } + } + } + + FutureOr _onEditSchedule(EditWaterHeaterScheduleEvent event, Emitter emit) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + ScheduleEntry newSchedule = ScheduleEntry( + scheduleId: event.scheduleId, + category: event.category, + time: formatTimeOfDayToISO(event.time), + function: Status(code: 'switch_1', value: event.functionOn), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), + ); + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi().editScheduleRecord( + currentState.status.uuid, + newSchedule, + ); + + if (success) { + add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); + } else { + emit(currentState); + //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); + } + } + } + + FutureOr _onUpdateSchedule( + UpdateScheduleEntryEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + final updatedSchedules = currentState.schedules.map((schedule) { + if (schedule.scheduleId == event.scheduleId) { + return schedule.copyWith( + function: Status(code: 'switch_1', value: event.functionOn), + enable: event.enable, + ); + } + return schedule; + }).toList(); + + bool success = await DevicesManagementApi().updateScheduleRecord( + enable: event.enable, + uuid: currentState.status.uuid, + scheduleId: event.scheduleId, + ); + + if (success) { + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); + } + } + } + + FutureOr _onDeleteSchedule( + DeleteScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi().deleteScheduleRecord(currentState.status.uuid, event.scheduleId); + + if (success) { + final updatedSchedules = + currentState.schedules.where((schedule) => schedule.scheduleId != event.scheduleId).toList(); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.')); + } + } + } + + FutureOr _batchFetchWaterHeater(FetchWaterHeaterBatchStatusEvent event, Emitter emit) async { + emit(WaterHeaterLoadingState()); + + try { + final status = await DevicesManagementApi().getBatchStatus(event.devicesUuid); + deviceStatus = WaterHeaterStatusModel.fromJson(event.devicesUuid.first, status.status); + + emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); + } catch (e) { + emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); + } + } + + FutureOr _batchControlWaterHeater(ControlWaterHeaterBatchEvent event, Emitter emit) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(currentState.copyWith( + status: deviceStatus, + )); + + final success = await _runDebounce( + deviceId: event.devicesUuid, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + + if (success) { + if (event.code == "switch_1") { + emit(currentState.copyWith( + status: deviceStatus, + )); + } + } else { + _updateLocalValue(event.code, oldValue); + } + } + } +} diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart new file mode 100644 index 00000000..ff5de32c --- /dev/null +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -0,0 +1,197 @@ +part of 'water_heater_bloc.dart'; + +sealed class WaterHeaterEvent extends Equatable { + const WaterHeaterEvent(); + + @override + List get props => []; +} + +final class ToggleWaterHeaterEvent extends WaterHeaterEvent { + final dynamic value; + final String deviceId; + final String code; + + const ToggleWaterHeaterEvent({ + required this.value, + required this.deviceId, + required this.code, + }); + + @override + List get props => [value, deviceId, code]; +} + +final class UpdateScheduleEvent extends WaterHeaterEvent { + final ScheduleModes scheduleMode; + final int hours; + final int minutes; + + const UpdateScheduleEvent({ + required this.scheduleMode, + required this.hours, + required this.minutes, + }); + + @override + List get props => [scheduleMode, hours, minutes]; +} + +final class StopScheduleEvent extends WaterHeaterEvent { + final String deviceId; + + const StopScheduleEvent(this.deviceId); +} + +final class WaterHeaterFetchStatusEvent extends WaterHeaterEvent { + final String deviceId; + + const WaterHeaterFetchStatusEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +final class DecrementCountdownEvent extends WaterHeaterEvent {} + +final class AddScheduleEvent extends WaterHeaterEvent { + final List selectedDays; + final TimeOfDay time; + final bool functionOn; + final String category; + + const AddScheduleEvent({ + required this.selectedDays, + required this.time, + required this.functionOn, + required this.category, + }); + + @override + List get props => [selectedDays, time, functionOn, category]; +} + +class EditWaterHeaterScheduleEvent extends WaterHeaterEvent { + final String scheduleId; + final String category; + final TimeOfDay time; + final bool functionOn; + final List selectedDays; + + const EditWaterHeaterScheduleEvent({ + required this.scheduleId, + required this.category, + required this.time, + required this.functionOn, + required this.selectedDays, + }); +} + +final class DeleteScheduleEvent extends WaterHeaterEvent { + final int index; + final String scheduleId; + + const DeleteScheduleEvent({required this.index, required this.scheduleId}); + + @override + List get props => [index, scheduleId]; +} + +final class UpdateScheduleEntryEvent extends WaterHeaterEvent { + final bool enable; + final dynamic functionOn; + final String deviceId; + final int index; + final String scheduleId; + + const UpdateScheduleEntryEvent({ + required this.enable, + required this.functionOn, + required this.deviceId, + required this.scheduleId, + required this.index, + }); + + @override + List get props => [enable, deviceId, scheduleId]; +} + +class GetSchedulesEvent extends WaterHeaterEvent { + final String uuid; + final String category; + + const GetSchedulesEvent({ + required this.uuid, + required this.category, + }); + + @override + List get props => [uuid, category]; +} + +class InitializeAddScheduleEvent extends WaterHeaterEvent { + final TimeOfDay? selectedTime; + final List? selectedDays; + final bool? functionOn; + final bool isEditing; + final int? index; + + const InitializeAddScheduleEvent({ + this.selectedTime, + this.selectedDays, + this.functionOn, + this.isEditing = false, + this.index, + }); +} + +class UpdateSelectedTimeEvent extends WaterHeaterEvent { + final TimeOfDay? selectedTime; + + const UpdateSelectedTimeEvent(this.selectedTime); + + @override + List get props => [selectedTime]; +} + +class UpdateSelectedDayEvent extends WaterHeaterEvent { + final int index; + final bool value; + + const UpdateSelectedDayEvent(this.index, this.value); + + @override + List get props => [index, value]; +} + +class UpdateFunctionOnEvent extends WaterHeaterEvent { + final bool isOn; + + const UpdateFunctionOnEvent(this.isOn); + + @override + List get props => [isOn]; +} + +class FetchWaterHeaterBatchStatusEvent extends WaterHeaterEvent { + final List devicesUuid; + const FetchWaterHeaterBatchStatusEvent({required this.devicesUuid}); + + @override + List get props => [devicesUuid]; +} + +class ControlWaterHeaterBatchEvent extends WaterHeaterEvent { + final List devicesUuid; + final String code; + final dynamic value; + + const ControlWaterHeaterBatchEvent({ + required this.devicesUuid, + required this.code, + required this.value, + }); + + @override + List get props => [devicesUuid, code, value]; +} diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart new file mode 100644 index 00000000..c2df43c3 --- /dev/null +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -0,0 +1,125 @@ +// water_heater_state.dart + +part of 'water_heater_bloc.dart'; + +sealed class WaterHeaterState extends Equatable { + const WaterHeaterState(); + + @override + List get props => []; +} + +final class WaterHeaterInitial extends WaterHeaterState {} + +final class WaterHeaterLoadingState extends WaterHeaterState {} + +final class ScheduleLoadingState extends WaterHeaterState {} + +class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { + final WaterHeaterStatusModel status; + final ScheduleModes? scheduleMode; + + // Countdown-specific + final int? countdownHours; + final int? countdownMinutes; + final Duration? countdownRemaining; + final bool? isCountdownActive; + + // Inching-specific + final int? inchingHours; + final int? inchingMinutes; + final bool? isInchingActive; + + final List schedules; + final List selectedDays; + final TimeOfDay? selectedTime; + final bool functionOn; + final bool isEditing; + + const WaterHeaterDeviceStatusLoaded( + this.status, { + this.scheduleMode, + this.countdownHours, + this.countdownMinutes, + this.countdownRemaining, + this.isCountdownActive, + this.inchingHours, + this.inchingMinutes, + this.isInchingActive, + this.schedules = const [], + this.selectedDays = const [false, false, false, false, false, false, false], + this.selectedTime, + this.functionOn = false, + this.isEditing = false, + }); + + @override + List get props => [ + status, + scheduleMode, + countdownHours, + countdownMinutes, + countdownRemaining, + isCountdownActive, + inchingHours, + inchingMinutes, + isInchingActive, + schedules, + selectedDays, + selectedTime, + functionOn, + isEditing + ]; + + WaterHeaterDeviceStatusLoaded copyWith({ + WaterHeaterStatusModel? status, + ScheduleModes? scheduleMode, + int? countdownHours, + int? countdownMinutes, + Duration? countdownRemaining, + bool? isCountdownActive, + int? inchingHours, + int? inchingMinutes, + bool? isInchingActive, + List? schedules, + List? selectedDays, + TimeOfDay? selectedTime, + bool? functionOn, + bool? isEditing, + }) { + return WaterHeaterDeviceStatusLoaded( + status ?? this.status, + scheduleMode: scheduleMode ?? this.scheduleMode, + countdownHours: countdownHours ?? this.countdownHours, + countdownMinutes: countdownMinutes ?? this.countdownMinutes, + countdownRemaining: countdownRemaining ?? this.countdownRemaining, + isCountdownActive: isCountdownActive ?? this.isCountdownActive, + inchingHours: inchingHours ?? this.inchingHours, + inchingMinutes: inchingMinutes ?? this.inchingMinutes, + isInchingActive: isInchingActive ?? this.isInchingActive, + schedules: schedules ?? this.schedules, + selectedDays: selectedDays ?? this.selectedDays, + selectedTime: selectedTime, + functionOn: functionOn ?? this.functionOn, + isEditing: isEditing ?? this.isEditing, + ); + } +} + +final class WaterHeaterFailedState extends WaterHeaterState { + final String error; + + const WaterHeaterFailedState({required this.error}); + + @override + List get props => [error]; +} + +final class WaterHeaterBatchFailedState extends WaterHeaterState { + final String error; + + const WaterHeaterBatchFailedState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart new file mode 100644 index 00000000..9278e396 --- /dev/null +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleDialogHelper { + static void showAddScheduleDialog(BuildContext context, {ScheduleModel? schedule, int? index, bool? isEdit}) { + final bloc = context.read(); + + if (schedule == null) { + bloc.add((const UpdateSelectedTimeEvent(null))); + bloc.add(InitializeAddScheduleEvent( + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + )); + } else { + final time = _convertStringToTimeOfDay(schedule.time); + final selectedDays = _convertDaysStringToBooleans(schedule.days); + + bloc.add(InitializeAddScheduleEvent( + selectedTime: time, + selectedDays: selectedDays, + functionOn: schedule.function.value, + isEditing: true, + index: index, + )); + } + + showDialog( + context: context, + builder: (ctx) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterDeviceStatusLoaded) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: DefaultButton( + padding: 8, + backgroundColor: ColorsManager.boxColor, + borderRadius: 15, + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: state.selectedTime ?? TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + ), + ), + child: child!, + ); + }, + ); + if (time != null) { + bloc.add(UpdateSelectedTimeEvent(time)); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + state.selectedTime == null ? 'Time' : state.selectedTime!.format(context), + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const Icon( + Icons.access_time, + color: ColorsManager.grayColor, + size: 18, + ), + ], + ), + ), + ), + const SizedBox(height: 16), + _buildDayCheckboxes(context, state.selectedDays, isEdit: isEdit), + const SizedBox(height: 16), + _buildFunctionSwitch(context, state.functionOn, isEdit), + ], + ), + actions: [ + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + if (state.selectedTime != null) { + if (state.isEditing && index != null) { + bloc.add(EditWaterHeaterScheduleEvent( + scheduleId: schedule?.scheduleId ?? '', + category: 'switch_1', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } else { + bloc.add(AddScheduleEvent( + category: 'switch_1', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } + Navigator.pop(context); + } + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } + return const SizedBox(); + }, + ), + ); + }, + ); + } + + static TimeOfDay _convertStringToTimeOfDay(String timeString) { + final regex = RegExp(r'^(\d{2}):(\d{2})$'); + final match = regex.firstMatch(timeString); + if (match != null) { + final hour = int.parse(match.group(1)!); + final minute = int.parse(match.group(2)!); + return TimeOfDay(hour: hour, minute: minute); + } else { + throw const FormatException('Invalid time format'); + } + } + + static List _convertDaysStringToBooleans(List selectedDays) { + final daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List daysBoolean = List.filled(7, false); + + for (int i = 0; i < daysOfWeek.length; i++) { + if (selectedDays.contains(daysOfWeek[i])) { + daysBoolean[i] = true; + } + } + + return daysBoolean; + } + + static Widget _buildDayCheckboxes(BuildContext context, List selectedDays, {bool? isEdit}) { + final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + return Row( + children: List.generate(7, (index) { + return Row( + children: [ + Checkbox( + value: selectedDays[index], + onChanged: (bool? value) { + context.read().add(UpdateSelectedDayEvent(index, value!)); + }, + ), + Text(dayLabels[index]), + ], + ); + }), + ); + } + + static Widget _buildFunctionSwitch(BuildContext context, bool isOn, bool? isEdit) { + return Row( + children: [ + Text( + 'Function:', + style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.grayColor), + ), + const SizedBox(width: 10), + Radio( + value: true, + groupValue: isOn, + onChanged: (bool? value) { + context.read().add(const UpdateFunctionOnEvent(true)); + }, + ), + const Text('On'), + const SizedBox(width: 10), + Radio( + value: false, + groupValue: isOn, + onChanged: (bool? value) { + context.read().add(const UpdateFunctionOnEvent(false)); + }, + ), + const Text('Off'), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart new file mode 100644 index 00000000..a2a109af --- /dev/null +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class ScheduleEntry { + final String category; + final String time; + final Status function; + final List days; + final String? scheduleId; + + ScheduleEntry({ + required this.category, + required this.time, + required this.function, + required this.days, + this.scheduleId, + }); + + @override + String toString() { + return 'ScheduleEntry(category: $category, time: $time, function: $function, days: $days)'; + } + + ScheduleEntry copyWith({ + String? category, + String? time, + Status? function, + List? days, + }) { + return ScheduleEntry( + category: category ?? this.category, + time: time ?? this.time, + function: function ?? this.function, + days: days ?? this.days, + ); + } + + Map toMap() { + return { + 'scheduleId': scheduleId, + 'category': category, + 'time': time, + 'function': function.toMap(), + 'days': days, + }; + } + + factory ScheduleEntry.fromMap(Map map) { + return ScheduleEntry( + category: map['category'] ?? '', + time: map['time'] ?? '', + function: Status.fromMap(map['function']), + days: List.from(map['days']), + ); + } + + String toJson() => json.encode(toMap()); + + factory ScheduleEntry.fromJson(String source) => ScheduleEntry.fromMap(json.decode(source)); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ScheduleEntry && + other.category == category && + other.time == time && + other.function == function && + listEquals(other.days, days); + } + + @override + int get hashCode { + return category.hashCode ^ time.hashCode ^ function.hashCode ^ days.hashCode; + } +} diff --git a/lib/pages/device_managment/water_heater/models/schedule_model.dart b/lib/pages/device_managment/water_heater/models/schedule_model.dart new file mode 100644 index 00000000..3d05a3e0 --- /dev/null +++ b/lib/pages/device_managment/water_heater/models/schedule_model.dart @@ -0,0 +1,132 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:flutter/foundation.dart'; + +class ScheduleModel { + final String scheduleId; + final String category; + final String time; + final Status function; + final List days; + final List? selectedDays; + final String timezoneId; + final bool enable; + + ScheduleModel({ + required this.scheduleId, + required this.category, + required this.time, + required this.function, + required this.days, + this.selectedDays, + required this.timezoneId, + required this.enable, + }); + + Map toMap() { + return { + 'scheduleId': scheduleId, + 'category': category, + 'time': time, + 'function': function.toMap(), + 'days': days, + 'timezoneId': timezoneId, + 'enable': enable, + }; + } + + factory ScheduleModel.fromMap(Map map) { + return ScheduleModel( + scheduleId: map['scheduleId'] ?? '', + category: map['category'] ?? '', + time: map['time'] ?? '', + function: Status.fromMap(map['function']), + days: List.from(map['days'].map((e) => e.toString())), + timezoneId: map['timezoneId'] ?? '', + enable: map['enable'] ?? false, + selectedDays: parseSelectedDays( + List.from(map['days'].map((e) => e.toString()))), + ); + } + + String toJson() => json.encode(toMap()); + + factory ScheduleModel.fromJson(String source) => + ScheduleModel.fromMap(json.decode(source)); + + ScheduleModel copyWith({ + String? scheduleId, + String? category, + String? time, + Status? function, + List? days, + List? selectedDays, + String? timezoneId, + bool? enable, + }) { + return ScheduleModel( + scheduleId: scheduleId ?? this.scheduleId, + category: category ?? this.category, + time: time ?? this.time, + function: function ?? this.function, + days: days ?? this.days, + selectedDays: selectedDays ?? this.selectedDays, + timezoneId: timezoneId ?? this.timezoneId, + enable: enable ?? this.enable, + ); + } + + static TimeOfDay? parseTimeOfDay(String isoTime) { + try { + final dateTime = DateTime.parse(isoTime); + return TimeOfDay(hour: dateTime.hour, minute: dateTime.minute); + } catch (e) { + return null; + } + } + + static List parseSelectedDays(List days) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return allDays.map((day) => days.contains(day)).toList(); + } + + static List convertSelectedDaysToStrings(List selectedDays) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List result = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + result.add(allDays[i]); + } + } + return result; + } + + @override + String toString() { + return 'ScheduleModel(category: $category, time: $time, function: $function, days: $days, selectedDays: $selectedDays)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ScheduleModel && + other.category == category && + other.time == time && + other.function == function && + listEquals(other.days, days) && + // timeOfDay == other.timeOfDay && + listEquals(other.selectedDays, selectedDays); + } + + @override + int get hashCode { + return category.hashCode ^ + time.hashCode ^ + function.hashCode ^ + days.hashCode ^ + // timeOfDay.hashCode ^ + selectedDays.hashCode; + } +} diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart new file mode 100644 index 00000000..c535bda2 --- /dev/null +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -0,0 +1,121 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; + +enum ScheduleModes { countdown, schedule, circulate, inching } + +class WaterHeaterStatusModel extends Equatable { + final String uuid; + final bool heaterSwitch; + final int countdownHours; + final int countdownMinutes; + final int inchingHours; + final int inchingMinutes; + final ScheduleModes scheduleMode; + final String relayStatus; + final String cycleTiming; + final List schedules; + + const WaterHeaterStatusModel({ + required this.uuid, + required this.heaterSwitch, + required this.countdownHours, + required this.countdownMinutes, + required this.relayStatus, + required this.cycleTiming, + required this.scheduleMode, + required this.schedules, + this.inchingHours = 0, + this.inchingMinutes = 0, + }); + + factory WaterHeaterStatusModel.fromJson(String id, List jsonList) { + late bool heaterSwitch = false; + late int countdownInSeconds = 0; + late String relayStatus = ''; + late String cycleTiming = ''; + late ScheduleModes scheduleMode = ScheduleModes.countdown; + + for (var status in jsonList) { + switch (status.code) { + case 'switch_1': + heaterSwitch = status.value ?? false; + break; + case 'countdown_1': + countdownInSeconds = status.value ?? 0; + break; + case 'relay_status': + relayStatus = status.value ?? 'memory'; + break; + case 'cycle_timing': + cycleTiming = status.value ?? ''; + break; + case 'switch_inching': + scheduleMode = getScheduleMode(status.value ?? 'countdown'); + break; + } + } + + final countdownHours = countdownInSeconds ~/ 3600; + final countdownMinutes = (countdownInSeconds % 3600) ~/ 60; + + return WaterHeaterStatusModel( + uuid: id, + heaterSwitch: heaterSwitch, + countdownHours: countdownHours, + countdownMinutes: countdownMinutes, + relayStatus: relayStatus, + cycleTiming: cycleTiming, + scheduleMode: scheduleMode, + schedules: const [], + ); + } + + WaterHeaterStatusModel copyWith({ + String? uuid, + bool? heaterSwitch, + int? countdownHours, + int? countdownMinutes, + String? relayStatus, + String? cycleTiming, + ScheduleModes? scheduleMode, + List? schedules, + }) { + return WaterHeaterStatusModel( + uuid: uuid ?? this.uuid, + heaterSwitch: heaterSwitch ?? this.heaterSwitch, + countdownHours: countdownHours ?? this.countdownHours, + countdownMinutes: countdownMinutes ?? this.countdownMinutes, + relayStatus: relayStatus ?? this.relayStatus, + cycleTiming: cycleTiming ?? this.cycleTiming, + scheduleMode: scheduleMode ?? this.scheduleMode, + schedules: schedules ?? this.schedules, + ); + } + + static ScheduleModes getScheduleMode(String value) { + switch (value) { + case 'countdown': + return ScheduleModes.countdown; + case 'schedule': + return ScheduleModes.schedule; + case 'circulate': + return ScheduleModes.circulate; + case 'inching': + return ScheduleModes.inching; + default: + return ScheduleModes.countdown; + } + } + + @override + List get props => [ + uuid, + heaterSwitch, + countdownHours, + countdownMinutes, + scheduleMode, + relayStatus, + cycleTiming, + ]; +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart new file mode 100644 index 00000000..cc62adfd --- /dev/null +++ b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WaterHEaterBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + const WaterHEaterBatchControlView({super.key, required this.deviceIds}); + + final List deviceIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WaterHeaterBloc() + ..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WaterHeaterDeviceStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is WaterHeaterBatchFailedState) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, WaterHeaterStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return SizedBox( + child: GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.heaterSwitch, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Water Heater', + icon: Assets.waterHeater, + onChange: (value) { + context.read().add( + ControlWaterHeaterBatchEvent( + devicesUuid: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget( + callFactoryReset: () {}, + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart new file mode 100644 index 00000000..40d3edb5 --- /dev/null +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WaterHeaterDeviceControlView extends StatelessWidget + with HelperResponsiveLayout { + const WaterHeaterDeviceControlView({super.key, required this.device}); + + final AllDevicesModel device; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + WaterHeaterBloc()..add(WaterHeaterFetchStatusEvent(device.uuid!)), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WaterHeaterDeviceStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is WaterHeaterFailedState || + state is WaterHeaterBatchFailedState) { + return const Center(child: Text('Error fetching status')); + } else { + return const SizedBox( + height: 200, child: Center(child: SizedBox())); + } + }, + )); + } + + Widget _buildStatusControls( + BuildContext context, + WaterHeaterStatusModel status, + ) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return GridView( + padding: const EdgeInsets.symmetric(horizontal: 150), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge || isMedium ? 2 : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + deviceId: device.uuid!, + code: 'switch_1', + value: status.heaterSwitch, + icon: Assets.waterHeater, + label: 'Water Heater', + onChange: (value) { + context.read().add(ToggleWaterHeaterEvent( + deviceId: device.uuid!, + code: 'switch_1', + value: value, + )); + }, + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView(status: status), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart b/lib/pages/device_managment/water_heater/widgets/count_down_button.dart new file mode 100644 index 00000000..e60c7def --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/count_down_button.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownModeButtons extends StatelessWidget { + final bool isActive; + final String deviceId; + final int hours; + final int minutes; + + const CountdownModeButtons({ + super.key, + required this.isActive, + required this.deviceId, + required this.hours, + required this.minutes, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () => Navigator.pop(context), + backgroundColor: ColorsManager.boxColor, + child: Text('Cancel', style: context.textTheme.bodyMedium), + ), + ), + const SizedBox(width: 20), + Expanded( + child: isActive + ? DefaultButton( + height: 40, + onPressed: () { + context + .read() + .add(StopScheduleEvent(deviceId)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'countdown_1', + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'countdown_1', + value: Duration(hours: hours, minutes: minutes) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart new file mode 100644 index 00000000..53892c20 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownInchingView extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const CountdownInchingView({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isCountDown ? 'Countdown:' : 'Inching:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 8), + Visibility( + visible: !isCountDown, + child: const Text( + 'Once enabled this feature, each time the device is turned on, it will automatically turn off after a preset time.'), + ), + const SizedBox(height: 8), + _hourMinutesWheel(context, state), + ], + ); + } + + Row _hourMinutesWheel( + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; + late bool isActive; + if (isCountDown && + state.countdownRemaining != null && + state.isCountdownActive == true) { + isActive = true; + } else if (!isCountDown && + state.countdownRemaining != null && + state.isInchingActive == true) { + isActive = true; + } else { + isActive = false; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPickerColumn( + context, + 'h', + isCountDown + ? (state.countdownHours ?? 0) + : (state.inchingHours ?? 0), + 24, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: value, + minutes: isCountDown + ? (state.countdownMinutes ?? 0) + : (state.inchingMinutes ?? 0), + )); + }, isActive: isActive), + const SizedBox(width: 10), + _buildPickerColumn( + context, + 'm', + isCountDown + ? (state.countdownMinutes ?? 0) + : (state.inchingMinutes ?? 0), + 60, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: isCountDown + ? (state.countdownHours ?? 0) + : (state.inchingHours ?? 0), + minutes: value, + )); + }, isActive: isActive), + ], + ); + } + + Widget _buildPickerColumn( + BuildContext context, + String label, + int initialValue, + int itemCount, + ValueChanged onSelected, { + required bool isActive, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListWheelScrollView.useDelegate( + key: ValueKey('$label-$initialValue'), + controller: FixedExtentScrollController( + initialItem: initialValue, + ), + itemExtent: 40.0, + physics: const FixedExtentScrollPhysics(), + onSelectedItemChanged: onSelected, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24, + color: isActive ? ColorsManager.grayColor : Colors.black, + ), + ), + ); + }, + childCount: itemCount, + ), + ), + ), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 18, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart b/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart new file mode 100644 index 00000000..8eec5cca --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class InchingModeButtons extends StatelessWidget { + final bool isActive; + final String deviceId; + final int hours; + final int minutes; + + const InchingModeButtons({ + Key? key, + required this.isActive, + required this.deviceId, + required this.hours, + required this.minutes, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () => Navigator.pop(context), + backgroundColor: ColorsManager.boxColor, + child: Text('Cancel', style: context.textTheme.bodyMedium), + ), + ), + const SizedBox(width: 20), + Expanded( + child: isActive + ? DefaultButton( + height: 40, + onPressed: () { + context + .read() + .add(StopScheduleEvent(deviceId)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'switch_inching', + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'switch_inching', + value: Duration(hours: hours, minutes: minutes) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart new file mode 100644 index 00000000..9d4a2497 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart'; + +class BuildScheduleView extends StatefulWidget { + const BuildScheduleView({super.key, required this.status}); + + final WaterHeaterStatusModel status; + + @override + State createState() => _BuildScheduleViewState(); +} + +class _BuildScheduleViewState extends State { + @override + Widget build(BuildContext context) { + final bloc = BlocProvider.of(context); + + return BlocProvider.value( + value: bloc, + child: Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 700, + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterDeviceStatusLoaded) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ScheduleHeader(), + const SizedBox(height: 20), + ScheduleModeSelector(state: state), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.schedule) + ScheduleManagementUI( + state: state, + onAddSchedule: () { + ScheduleDialogHelper.showAddScheduleDialog( + context, + schedule: null, + index: null, + isEdit: false); + }, + ), + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + CountdownInchingView(state: state), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.countdown) + CountdownModeButtons( + isActive: state.isCountdownActive ?? false, + deviceId: widget.status.uuid, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, + ), + if (state.scheduleMode == ScheduleModes.inching) + InchingModeButtons( + isActive: state.isInchingActive ?? false, + deviceId: widget.status.uuid, + hours: state.inchingHours ?? 0, + minutes: state.inchingMinutes ?? 0, + ), + if (state.scheduleMode != ScheduleModes.countdown && + state.scheduleMode != ScheduleModes.inching) + ScheduleModeButtons( + onSave: () { + Navigator.pop(context); + }, + ), + ], + ); + } + if (state is WaterHeaterLoadingState) { + return const SizedBox( + height: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ScheduleHeader(), + SizedBox( + height: 20, + ), + Center(child: CircularProgressIndicator()), + ], + )); + } + return const SizedBox( + height: 200, + child: ScheduleHeader(), + ); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_header.dart b/lib/pages/device_managment/water_heater/widgets/schedule_header.dart new file mode 100644 index 00000000..87afe430 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_header.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleHeader extends StatelessWidget { + const ScheduleHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: const EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart b/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart new file mode 100644 index 00000000..1710c439 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_table.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleManagementUI extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + final Function onAddSchedule; + + const ScheduleManagementUI({ + super.key, + required this.state, + required this.onAddSchedule, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 170, + height: 40, + child: DefaultButton( + borderColor: ColorsManager.boxColor, + padding: 2, + backgroundColor: ColorsManager.graysColor, + borderRadius: 15, + onPressed: () => onAddSchedule(), + child: Row( + children: [ + const Icon(Icons.add, color: ColorsManager.primaryColor), + Text( + ' Add new schedule', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ScheduleTableWidget(state: state), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart new file mode 100644 index 00000000..f1307d5f --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleModeButtons extends StatelessWidget { + final VoidCallback onSave; + + const ScheduleModeButtons({ + super.key, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: DefaultButton( + height: 40, + onPressed: onSave, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart new file mode 100644 index 00000000..bb9ddc8f --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; + +class ScheduleModeSelector extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const ScheduleModeSelector({super.key, required this.state}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildRadioTile( + context, 'Countdown', ScheduleModes.countdown, state), + _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), + _buildRadioTile( + context, 'Circulate', ScheduleModes.circulate, state), + _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), + ], + ), + ], + ); + } + + Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, + WaterHeaterDeviceStatusLoaded state) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: state.scheduleMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + if (value == ScheduleModes.countdown) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, + )); + } else if (value == ScheduleModes.inching) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.inchingHours ?? 0, + minutes: state.inchingMinutes ?? 0, + )); + } + + if (value == ScheduleModes.schedule) { + context.read().add( + GetSchedulesEvent( + category: 'switch_1', + uuid: state.status.uuid, + ), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart new file mode 100644 index 00000000..18cbbe5a --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; + +import '../helper/add_schedule_dialog_helper.dart'; + +class ScheduleTableWidget extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const ScheduleTableWidget({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Table( + border: TableBorder.all( + color: ColorsManager.graysColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), + children: [ + TableRow( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + children: [ + _buildTableHeader('Active'), + _buildTableHeader('Days'), + _buildTableHeader('Time'), + _buildTableHeader('Function'), + _buildTableHeader('Action'), + ], + ), + ], + ), + BlocBuilder( + builder: (context, state) { + if (state is ScheduleLoadingState) { + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator())); + } + if (state is WaterHeaterDeviceStatusLoaded && + state.schedules.isEmpty) { + return _buildEmptyState(context); + } else if (state is WaterHeaterDeviceStatusLoaded) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20)), + ), + child: _buildTableBody(state, context)); + } + return const SizedBox( + height: 200, + ); + }, + ), + ], + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'No schedules added yet', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTableBody( + WaterHeaterDeviceStatusLoaded state, BuildContext context) { + return SizedBox( + height: 200, + child: SingleChildScrollView( + child: Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (int i = 0; i < state.schedules.length; i++) + _buildScheduleRow(state.schedules[i], i, context, state), + ], + ), + ), + ); + } + + Widget _buildTableHeader(String label) { + return TableCell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + label, + style: const TextStyle( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ); + } + + TableRow _buildScheduleRow(ScheduleModel schedule, int index, + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + return TableRow( + children: [ + Center( + child: GestureDetector( + onTap: () { + context.read().add(UpdateScheduleEntryEvent( + index: index, + enable: !schedule.enable, + scheduleId: schedule.scheduleId, + deviceId: state.status.uuid, + functionOn: schedule.function.value, + )); + }, + child: SizedBox( + width: 24, + height: 24, + child: schedule.enable + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon( + Icons.radio_button_unchecked, + color: ColorsManager.grayColor, + ), + ), + ), + ), + Center( + child: Text(_getSelectedDays( + ScheduleModel.parseSelectedDays(schedule.days)))), + Center(child: Text(formatIsoStringToTime(schedule.time, context))), + Center(child: Text(schedule.function.value ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + ScheduleDialogHelper.showAddScheduleDialog(context, + schedule: schedule, index: index, isEdit: true); + }, + child: Text( + 'Edit', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + context.read().add(DeleteScheduleEvent( + index: index, + scheduleId: schedule.scheduleId, + )); + }, + child: Text( + 'Delete', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } +} diff --git a/lib/pages/device_managment/water_leak/bloc/water_leak_bloc.dart b/lib/pages/device_managment/water_leak/bloc/water_leak_bloc.dart new file mode 100644 index 00000000..f1063dc9 --- /dev/null +++ b/lib/pages/device_managment/water_leak/bloc/water_leak_bloc.dart @@ -0,0 +1,173 @@ +import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_event.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_state.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/model/water_leak_status_model.dart'; + +import 'dart:async'; + +import 'package:syncrow_web/services/devices_mang_api.dart'; + +class WaterLeakBloc extends Bloc { + WaterLeakStatusModel? deviceStatus; + Timer? _timer; + final String deviceId; + + WaterLeakBloc(this.deviceId) : super(WaterLeakInitialState()) { + on(_onFetchWaterLeakStatus); + on(_onControl); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFetchWaterLeakReports); + on(_onFactoryReset); + } + + Future _onFetchWaterLeakStatus( + FetchWaterLeakStatusEvent event, Emitter emit) async { + emit(WaterLeakLoadingState()); + try { + final response = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = WaterLeakStatusModel.fromJson(deviceId, response.status); + emit(WaterLeakLoadedState(deviceStatus!)); + } catch (e) { + emit(WaterLeakErrorState(e.toString())); + } + } + + Future _onControl( + WaterLeakControlEvent event, Emitter emit) async { + final oldValue = deviceStatus!.watersensorState; + + _updateLocalValue(event.code, event.value); + emit(WaterLeakLoadedState(deviceStatus!)); + + await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: false, + ); + } + + Future _onFactoryReset( + WaterLeakFactoryResetEvent event, Emitter emit) async { + emit(WaterLeakLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (response) { + emit(WaterLeakInitialState()); + } else { + emit(const WaterLeakErrorState('Factory reset failed')); + } + } catch (e) { + emit(WaterLeakErrorState(e.toString())); + } + } + + Future _onBatchControl( + WaterLeakBatchControlEvent event, Emitter emit) async { + final oldValue = deviceStatus!.watersensorState; + + _updateLocalValue(event.code, event.value); + emit(WaterLeakBatchStatusLoadedState(deviceStatus!)); + + await _runDebounce( + deviceId: event.deviceIds, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + } + + Future _onFetchBatchStatus(FetchWaterLeakBatchStatusEvent event, + Emitter emit) async { + emit(WaterLeakLoadingState()); + try { + final response = + await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = WaterLeakStatusModel.fromJson(deviceId, response.status); + emit(WaterLeakBatchStatusLoadedState(deviceStatus!)); + } catch (e) { + emit(WaterLeakErrorState(e.toString())); + } + } + + Future _runDebounce({ + required dynamic deviceId, + required String code, + required dynamic value, + required dynamic oldValue, + required Emitter emit, + required bool isBatch, + }) async { + late String id; + if (deviceId is List) { + id = deviceId.first; + } else { + id = deviceId; + } + + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(const Duration(milliseconds: 500), () async { + try { + late bool response; + if (isBatch) { + response = await DevicesManagementApi() + .deviceBatchControl(deviceId, code, value); + } else { + response = await DevicesManagementApi() + .deviceControl(deviceId, Status(code: code, value: value)); + } + + if (!response) { + _revertValueAndEmit(id, code, oldValue, emit); + } + } catch (e) { + _revertValueAndEmit(id, code, oldValue, emit); + } + }); + } + + void _updateLocalValue(String code, dynamic value) { + if (code == 'watersensor_state') { + deviceStatus = deviceStatus!.copyWith(watersensorState: value); + } else if (code == 'battery_percentage') { + deviceStatus = deviceStatus!.copyWith(batteryPercentage: value); + } + } + + void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, + Emitter emit) { + _updateLocalValue(code, oldValue); + emit(WaterLeakLoadedState(deviceStatus!)); + } + + Future _onFetchWaterLeakReports( + FetchWaterLeakReportsEvent event, Emitter emit) async { + emit(WaterLeakReportsLoadingState()); + try { + final from = DateTime.now() + .subtract(const Duration(days: 30)) + .millisecondsSinceEpoch; + final to = DateTime.now().millisecondsSinceEpoch; + final DeviceReport records = + await DevicesManagementApi.getDeviceReportsByDate( + event.deviceId, event.code, from.toString(), to.toString()); + emit(WaterLeakReportsLoadedState(records)); + } catch (e) { + emit(WaterLeakReportsFailedState(e.toString())); + } + } +} diff --git a/lib/pages/device_managment/water_leak/bloc/water_leak_event.dart b/lib/pages/device_managment/water_leak/bloc/water_leak_event.dart new file mode 100644 index 00000000..9c048280 --- /dev/null +++ b/lib/pages/device_managment/water_leak/bloc/water_leak_event.dart @@ -0,0 +1,87 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + +abstract class WaterLeakEvent extends Equatable { + const WaterLeakEvent(); + + @override + List get props => []; +} + +class FetchWaterLeakStatusEvent extends WaterLeakEvent { + final String deviceId; + + const FetchWaterLeakStatusEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class WaterLeakControlEvent extends WaterLeakEvent { + final String deviceId; + final String code; + final dynamic value; + + const WaterLeakControlEvent({ + required this.deviceId, + required this.code, + required this.value, + }); + + @override + List get props => [deviceId, code, value]; +} + +class WaterLeakBatchControlEvent extends WaterLeakEvent { + final List deviceIds; + final String code; + final dynamic value; + + const WaterLeakBatchControlEvent({ + required this.deviceIds, + required this.code, + required this.value, + }); + + @override + List get props => [deviceIds, code, value]; +} + +class FetchWaterLeakBatchStatusEvent extends WaterLeakEvent { + final List deviceIds; + + const FetchWaterLeakBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; +} + +class FetchWaterLeakReportsEvent extends WaterLeakEvent { + final String deviceId; + final String code; + final int from; + final int to; + + const FetchWaterLeakReportsEvent({ + required this.deviceId, + required this.code, + required this.from, + required this.to, + }); + + @override + List get props => [deviceId, code, from, to]; +} + +class WaterLeakFactoryResetEvent extends WaterLeakEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const WaterLeakFactoryResetEvent({ + required this.deviceId, + required this.factoryReset, + }); + + @override + List get props => [deviceId]; +} diff --git a/lib/pages/device_managment/water_leak/bloc/water_leak_state.dart b/lib/pages/device_managment/water_leak/bloc/water_leak_state.dart new file mode 100644 index 00000000..e1dc0de0 --- /dev/null +++ b/lib/pages/device_managment/water_leak/bloc/water_leak_state.dart @@ -0,0 +1,61 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/model/water_leak_status_model.dart'; + +abstract class WaterLeakState extends Equatable { + const WaterLeakState(); + + @override + List get props => []; +} + +class WaterLeakInitialState extends WaterLeakState {} + +class WaterLeakLoadingState extends WaterLeakState {} + +class WaterLeakLoadedState extends WaterLeakState { + final WaterLeakStatusModel status; + + const WaterLeakLoadedState(this.status); + + @override + List get props => [status]; +} + +class WaterLeakBatchStatusLoadedState extends WaterLeakState { + final WaterLeakStatusModel status; + + const WaterLeakBatchStatusLoadedState(this.status); + + @override + List get props => [status]; +} + +class WaterLeakErrorState extends WaterLeakState { + final String message; + + const WaterLeakErrorState(this.message); + + @override + List get props => [message]; +} + +class WaterLeakReportsLoadingState extends WaterLeakState {} + +class WaterLeakReportsLoadedState extends WaterLeakState { + final DeviceReport deviceReport; + + const WaterLeakReportsLoadedState(this.deviceReport); + + @override + List get props => [deviceReport]; +} + +class WaterLeakReportsFailedState extends WaterLeakState { + final String error; + + const WaterLeakReportsFailedState(this.error); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/water_leak/model/water_leak_status_model.dart b/lib/pages/device_managment/water_leak/model/water_leak_status_model.dart new file mode 100644 index 00000000..d496c969 --- /dev/null +++ b/lib/pages/device_managment/water_leak/model/water_leak_status_model.dart @@ -0,0 +1,48 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +class WaterLeakStatusModel { + final String productUuid; + final String productType; + final String watersensorState; + final int batteryPercentage; + + WaterLeakStatusModel({ + required this.productUuid, + required this.productType, + required this.watersensorState, + required this.batteryPercentage, + }); + factory WaterLeakStatusModel.fromJson(String id, List jsonList) { + late String watersensorState; + late int batteryPercentage; + + for (var i = 0; i < jsonList.length; i++) { + if (jsonList[i].code == 'watersensor_state') { + watersensorState = jsonList[i].value; + } else if (jsonList[i].code == 'battery_percentage') { + batteryPercentage = jsonList[i].value; + } + } + + return WaterLeakStatusModel( + productUuid: id, + productType: 'WL', + watersensorState: watersensorState, + batteryPercentage: batteryPercentage, + ); + } + + WaterLeakStatusModel copyWith({ + String? productUuid, + String? productType, + String? watersensorState, + int? batteryPercentage, + }) { + return WaterLeakStatusModel( + productUuid: productUuid ?? this.productUuid, + productType: productType ?? this.productType, + watersensorState: watersensorState ?? this.watersensorState, + batteryPercentage: batteryPercentage ?? this.batteryPercentage, + ); + } +} diff --git a/lib/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart b/lib/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart new file mode 100644 index 00000000..9d2c030f --- /dev/null +++ b/lib/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; + +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_event.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_state.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/model/water_leak_status_model.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WaterLeakBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + final List deviceIds; + + const WaterLeakBatchControlView({Key? key, required this.deviceIds}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WaterLeakBloc(deviceIds.first) + ..add(FetchWaterLeakBatchStatusEvent(deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterLeakLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WaterLeakBatchStatusLoadedState) { + return _buildStatusControls(context, state.status); + } else if (state is WaterLeakErrorState) { + return Center(child: Text('Error: ${state.message}')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, WaterLeakStatusModel status) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 170, + height: 140, + child: FirmwareUpdateWidget(deviceId: deviceIds.first, version: 2)), + const SizedBox( + width: 12, + ), + SizedBox( + width: 170, + height: 140, + child: FactoryResetWidget( + callFactoryReset: () { + context.read().add(WaterLeakFactoryResetEvent( + deviceId: deviceIds.first, + factoryReset: FactoryResetModel(devicesUuid: deviceIds))); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_leak/view/water_leak_control_view.dart b/lib/pages/device_managment/water_leak/view/water_leak_control_view.dart new file mode 100644 index 00000000..6b9dc564 --- /dev/null +++ b/lib/pages/device_managment/water_leak/view/water_leak_control_view.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_event.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/bloc/water_leak_state.dart'; +import 'package:syncrow_web/pages/device_managment/water_leak/widgets/water_leak_notifi_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WaterLeakView extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + + const WaterLeakView({Key? key, required this.deviceId}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return BlocProvider( + create: (context) => WaterLeakBloc(deviceId)..add(FetchWaterLeakStatusEvent(deviceId)), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterLeakLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WaterLeakLoadedState) { + return GridView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 50), + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isExtraLarge || isLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + IconNameStatusContainer( + isFullIcon: false, + name: state.status.watersensorState == 'normal' ? 'Normal' : 'Leak Detection', + icon: state.status.watersensorState == 'normal' ? Assets.waterLeakNormal : Assets.waterLeakDetected, + onTap: () {}, + status: state.status.watersensorState == 'normal', + textColor: state.status.watersensorState == 'normal' ? ColorsManager.blackColor : ColorsManager.red, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Records', + icon: Assets.records, + onTap: () { + context.read().add(FetchWaterLeakReportsEvent( + deviceId: deviceId, + code: 'watersensor_state', + from: DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch, + to: DateTime.now().millisecondsSinceEpoch, + )); + }, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Automation Record', + icon: Assets.automationRecords, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + IconNameStatusContainer( + isFullIcon: false, + name: 'Notifications\nSettings', + icon: Assets.mainDoorNotifi, + onTap: () { + showDialog( + context: context, + builder: (context) => const WaterLeakNotificationDialog(), + ); + }, + status: false, + textColor: ColorsManager.blackColor, + paddingAmount: 14, + ), + ], + ); + } else if (state is WaterLeakReportsLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WaterLeakReportsLoadedState) { + return ReportsTable( + report: state.deviceReport, + hideValueShowDescription: true, + waterLeak: true, + onRowTap: (index) {}, + onClose: () { + context.read().add(FetchWaterLeakStatusEvent(deviceId)); + }, + ); + } else if (state is WaterLeakReportsFailedState) { + return Center(child: Text('Error: ${state.error}')); + } else if (state is WaterLeakErrorState) { + return Center(child: Text('Error: ${state.message}')); + } else { + return const Center(child: Text('No data available')); + } + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/water_leak/widgets/water_leak_notifi_dialog.dart b/lib/pages/device_managment/water_leak/widgets/water_leak_notifi_dialog.dart new file mode 100644 index 00000000..0e3e325f --- /dev/null +++ b/lib/pages/device_managment/water_leak/widgets/water_leak_notifi_dialog.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WaterLeakNotificationDialog extends StatefulWidget { + const WaterLeakNotificationDialog({super.key}); + + @override + State createState() => _NotificationDialogState(); +} + +class _NotificationDialogState extends State { + bool isLowBatteryNotificationEnabled = true; + bool isClosingRemindersEnabled = true; + bool isWaterLeakage = true; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 560, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Notification Settings', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isLowBatteryNotificationEnabled, + code: 'notification', + deviceId: '', + label: 'Low Battery', + onChange: (v) { + setState(() { + isLowBatteryNotificationEnabled = v; + }); + }, + icon: '-1', + ), + ), + SizedBox( + width: 170, + height: 135, + child: ToggleWidget( + value: isWaterLeakage, + code: 'notification', + deviceId: '', + label: 'Water Leakage', + onChange: (v) { + setState(() { + isWaterLeakage = v; + }); + }, + icon: '-1', + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index 7993c1ce..32f3a5c0 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -16,11 +16,11 @@ class HomeBloc extends Bloc { final BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); List sourcesList = []; List destinationsList = []; - static UserModel? user; + UserModel? user; HomeBloc() : super((HomeInitial())) { on(_createNode); - fetchUserInfo(); + on(_fetchUserInfo); } void _createNode(CreateNewNode event, Emitter emit) async { @@ -39,17 +39,27 @@ class HomeBloc extends Bloc { emit(HomeUpdateTree(graph: graph, builder: builder)); } - Future fetchUserInfo() async { + Future _fetchUserInfo(FetchUserInfo event, Emitter emit) async { try { var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); user = await HomeApi().fetchUserInfo(uuid); - emit(HomeUserInfoLoaded(user!)); // Emit state after fetching user info + emit(HomeInitial()); } catch (e) { return; } } +// static Future fetchUserInfo() async { +// try { +// var uuid = +// await const FlutterSecureStorage().read(key: UserModel.userUuidKey); +// user = await HomeApi().fetchUserInfo(uuid); +// } catch (e) { +// return; +// } +// } + List homeItems = [ HomeItemModel( title: 'Access', diff --git a/lib/pages/home/bloc/home_event.dart b/lib/pages/home/bloc/home_event.dart index da517943..963202b9 100644 --- a/lib/pages/home/bloc/home_event.dart +++ b/lib/pages/home/bloc/home_event.dart @@ -17,3 +17,7 @@ class CreateNewNode extends HomeEvent { @override List get props => [sourceNode, destinationNode]; } + +class FetchUserInfo extends HomeEvent { + const FetchUserInfo(); +} \ No newline at end of file diff --git a/lib/pages/home/bloc/home_state.dart b/lib/pages/home/bloc/home_state.dart index dda3fa50..10c50486 100644 --- a/lib/pages/home/bloc/home_state.dart +++ b/lib/pages/home/bloc/home_state.dart @@ -1,6 +1,5 @@ import 'package:equatable/equatable.dart'; import 'package:graphview/GraphView.dart'; -import 'package:syncrow_web/pages/auth/model/user_model.dart'; abstract class HomeState extends Equatable { const HomeState(); @@ -25,9 +24,3 @@ class HomeUpdateTree extends HomeState { @override List get props => [graph, builder]; } - -class HomeUserInfoLoaded extends HomeState { - final UserModel user; - - HomeUserInfoLoaded(this.user); -} diff --git a/lib/pages/home/view/home_page.dart b/lib/pages/home/view/home_page.dart index c1e36729..9159011f 100644 --- a/lib/pages/home/view/home_page.dart +++ b/lib/pages/home/view/home_page.dart @@ -1,13 +1,17 @@ import 'package:flutter/cupertino.dart'; import 'package:syncrow_web/pages/home/view/home_page_mobile.dart'; import 'package:syncrow_web/pages/home/view/home_page_web.dart'; -import 'package:syncrow_web/utils/responsive_layout.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class HomePage extends StatelessWidget { +class HomePage extends StatelessWidget with HelperResponsiveLayout { const HomePage({super.key}); @override Widget build(BuildContext context) { - return ResponsiveLayout(desktopBody: HomeWebPage(), mobileBody: HomeMobilePage()); + final isSmallScreen = isSmallScreenSize(context); + final isMediumScreen = isMediumScreenSize(context); + return isSmallScreen || isMediumScreen + ? HomeMobilePage() + : const HomeWebPage(); } } diff --git a/lib/pages/home/view/home_page_mobile.dart b/lib/pages/home/view/home_page_mobile.dart index dcf35a41..8f72f8cb 100644 --- a/lib/pages/home/view/home_page_mobile.dart +++ b/lib/pages/home/view/home_page_mobile.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/pages/home/view/home_card.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -9,68 +10,77 @@ import 'package:syncrow_web/web_layout/web_scaffold.dart'; class HomeMobilePage extends StatelessWidget { HomeMobilePage({super.key}); + @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; - return WebScaffold( - enableMenuSideba: false, - appBarTitle: Row( - children: [ - SvgPicture.asset( - Assets.loginLogo, - width: 150, - ), - ], - ), - scaffoldBody: BlocProvider( - create: (context) => HomeBloc(), - child: SizedBox( - height: size.height, - width: size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: size.height * 0.05), - const Text( - 'ACCESS YOUR APPS', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700), - ), - const SizedBox(height: 30), - Expanded( - flex: 4, - child: SizedBox( - height: size.height * 0.6, - width: size.width * 0.68, - child: GridView.builder( - itemCount: 8, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 20.0, - mainAxisSpacing: 20.0, - childAspectRatio: 1.5, - ), - itemBuilder: (context, index) { - return HomeCard( - index: index, - active: homeItems[index]['active'], - name: homeItems[index]['title'], - img: homeItems[index]['icon'], - onTap: () {}, - ); - }, - ), - ), - ), - ], - ), + return PopScope( + canPop: false, + onPopInvoked: (didPop) => false, + child: WebScaffold( + enableMenuSidebar: false, + appBarTitle: Row( + children: [ + SvgPicture.asset( + Assets.loginLogo, + width: 150, + ), + ], ), + scaffoldBody: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + final homeBloc = BlocProvider.of(context); + return SizedBox( + height: size.height, + width: size.width, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: size.height * 0.05), + const Text( + 'ACCESS YOUR APPS', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 30), + Expanded( + flex: 4, + child: SizedBox( + height: size.height * 0.6, + width: size.width * 0.68, + child: GridView.builder( + itemCount: 8, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 20.0, + mainAxisSpacing: 20.0, + childAspectRatio: 1.5, + ), + itemBuilder: (context, index) { + return HomeCard( + index: index, + active: homeItems[index]['active'], + name: homeItems[index]['title'], + img: homeItems[index]['icon'], + onTap: () => + homeBloc.homeItems[index].onPress(context), + ); + }, + ), + ), + ), + ], + ), + ); + }), ), ); } - dynamic homeItems = [ + final dynamic homeItems = [ { 'title': 'Access', 'icon': Assets.accessIcon, diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index 2a578fc9..cb806dfc 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -8,31 +8,28 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; class HomeWebPage extends StatelessWidget { - HomeWebPage({super.key}); + const HomeWebPage({super.key}); @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; return PopScope( canPop: false, onPopInvoked: (didPop) => false, - child: - WebScaffold( - enableMenuSideba: false, - appBarTitle: Row( - children: [ - SvgPicture.asset( - Assets.loginLogo, - width: 150, - ), - ], - ), - scaffoldBody: BlocProvider( - create: (context) => HomeBloc(), - child: BlocConsumer( - listener: (BuildContext context, state) {}, - builder: (context, state) { - final homeBloc = BlocProvider.of(context); - return SizedBox( + child: BlocConsumer( + listener: (BuildContext context, state) {}, + builder: (context, state) { + final homeBloc = BlocProvider.of(context); + return WebScaffold( + enableMenuSidebar: false, + appBarTitle: Row( + children: [ + SvgPicture.asset( + Assets.loginLogo, + width: 150, + ), + ], + ), + scaffoldBody: SizedBox( height: size.height, width: size.width, child: Column( @@ -55,8 +52,7 @@ class HomeWebPage extends StatelessWidget { width: size.width * 0.68, child: GridView.builder( itemCount: 8, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, @@ -68,8 +64,7 @@ class HomeWebPage extends StatelessWidget { active: homeBloc.homeItems[index].active!, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, - onTap: () => - homeBloc.homeItems[index].onPress(context), + onTap: () => homeBloc.homeItems[index].onPress(context), ); }, ), @@ -77,9 +72,9 @@ class HomeWebPage extends StatelessWidget { ), ], ), - ); - }, - ), - ))); + ), + ); + }, + )); } } diff --git a/lib/pages/space_management/spaseManagementIcon.dart b/lib/pages/space_management/spaseManagementIcon.dart deleted file mode 100644 index 510b90b9..00000000 --- a/lib/pages/space_management/spaseManagementIcon.dart +++ /dev/null @@ -1,16 +0,0 @@ - - - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class SpaseManagementicon extends StatelessWidget { - const SpaseManagementicon({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container(), - ); - } -} diff --git a/lib/pages/spaces_management/view/spaces_management_page.dart b/lib/pages/spaces_management/view/spaces_management_page.dart index ac135011..8f3d5329 100644 --- a/lib/pages/spaces_management/view/spaces_management_page.dart +++ b/lib/pages/spaces_management/view/spaces_management_page.dart @@ -89,7 +89,7 @@ class SpaceManagementPageState extends State { 'Space Management', style: Theme.of(context).textTheme.headlineLarge, ), - enableMenuSideba: false, + enableMenuSidebar: false, scaffoldBody: BlocBuilder( builder: (context, state) { if (state is SpaceManagementLoading) { diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 2b64d606..aeb53260 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -32,9 +32,9 @@ class VisitorPasswordBloc extends Bloc(selectTimeOfLinePassword); on(changeTime); } + final TextEditingController userNameController = TextEditingController(); final TextEditingController emailController = TextEditingController(); - final TextEditingController deviceNameController = TextEditingController(); final TextEditingController deviceIdController = TextEditingController(); final TextEditingController unitNameController = TextEditingController(); @@ -51,6 +51,7 @@ class VisitorPasswordBloc extends Bloc mapEventToState(VisitorPasswordEvent event) async* { if (event is FetchDevice) { } else if (event is UpdateFilteredDevicesEvent) { @@ -357,13 +353,14 @@ class VisitorPasswordBloc extends Bloc expirationTimeTimeStamp!) { - CustomSnackBar.displaySnackBar('Effective Time cannot be later than Expiration Time.'); + accessPeriodValidate = "Effective Time cannot be later than Expiration Time."; } else { - effectiveTime = - selectedDateTime.toString().split('.').first; // Remove seconds and milliseconds + accessPeriodValidate = ''; + effectiveTime = selectedDateTime.toString().split('.').first; effectiveTimeTimeStamp = selectedTimestamp; } } else { if (effectiveTimeTimeStamp != null && selectedTimestamp < effectiveTimeTimeStamp!) { - CustomSnackBar.displaySnackBar( - 'Expiration Time cannot be earlier than Effective Time.'); + accessPeriodValidate = 'Expiration Time cannot be earlier than Effective Time.'; } else { - expirationTime = - selectedDateTime.toString().split('.').first; // Remove seconds and milliseconds + accessPeriodValidate = ''; + expirationTime = selectedDateTime.toString().split('.').first; expirationTimeTimeStamp = selectedTimestamp; } } @@ -417,7 +413,6 @@ class VisitorPasswordBloc extends Bloc json) { String tempIcon = ''; DeviceType type = devicesTypesMap[json['productType']] ?? DeviceType.Other; - if (type == DeviceType.LightBulb) { tempIcon = Assets.lightBulb; } else if (type == DeviceType.CeilingSensor || type == DeviceType.WallSensor) { @@ -66,9 +65,22 @@ class DeviceModel { tempIcon = Assets.gangSwitch; } else if (type == DeviceType.Gateway) { tempIcon = Assets.gateway; + } else if (type == DeviceType.OneGang) { + tempIcon = Assets.oneGang; + } else if (type == DeviceType.TwoGang) { + tempIcon = Assets.twoGang; + } else if (type == DeviceType.WH) { + tempIcon = Assets.waterHeater; + } else if (type == DeviceType.DoorSensor) { + tempIcon = Assets.openCloseDoor; + } else if (type == DeviceType.GarageDoor) { + tempIcon = Assets.openedDoor; + } else if (type == DeviceType.WaterLeak) { + tempIcon = Assets.waterLeakNormal; } else { - tempIcon = Assets.logo; + tempIcon = Assets.blackLogo; } + return DeviceModel( productUuid: json['productUuid'], productType: json['productType'], diff --git a/lib/pages/visitor_password/view/add_device_dialog.dart b/lib/pages/visitor_password/view/add_device_dialog.dart index 85262ff1..ebc41292 100644 --- a/lib/pages/visitor_password/view/add_device_dialog.dart +++ b/lib/pages/visitor_password/view/add_device_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/common/custom_table.dart'; +import 'package:syncrow_web/pages/common/access_device_table.dart'; import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; @@ -35,12 +35,12 @@ class AddDeviceDialog extends StatelessWidget { backgroundColor: Colors.white, title: Text( 'Add Accessible Device', - style: Theme.of(context).textTheme.headlineLarge!.copyWith( - fontWeight: FontWeight.w400, - fontSize: 24, - color: Colors.black), + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith(fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), ), - content: Container( + content: SizedBox( height: MediaQuery.of(context).size.height / 1.7, width: MediaQuery.of(context).size.width / 2, child: Padding( @@ -49,7 +49,7 @@ class AddDeviceDialog extends StatelessWidget { children: [ Container( width: size.width, - padding: EdgeInsets.all(15), + padding: const EdgeInsets.all(15), decoration: containerDecoration.copyWith( color: ColorsManager.worningColor, border: Border.all(color: Color(0xffFFD22F)), @@ -68,17 +68,14 @@ class AddDeviceDialog extends StatelessWidget { ), Text( 'Only online accessible devices can be added', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - fontWeight: FontWeight.w400, - fontSize: 12, - color: ColorsManager.grayColor), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.grayColor), ), ], )), - SizedBox( + const SizedBox( height: 20, ), const SizedBox( @@ -93,7 +90,7 @@ class AddDeviceDialog extends StatelessWidget { flex: 4, child: CustomWebTextField( controller: visitorBloc.deviceNameController, - isRequired: true, + isRequired: false, textFieldName: 'Device Name', description: '', ), @@ -103,7 +100,7 @@ class AddDeviceDialog extends StatelessWidget { flex: 4, child: CustomWebTextField( controller: visitorBloc.deviceIdController, - isRequired: true, + isRequired: false, textFieldName: 'Device ID', description: '', ), @@ -113,8 +110,8 @@ class AddDeviceDialog extends StatelessWidget { flex: 4, child: CustomWebTextField( controller: visitorBloc.unitNameController, - isRequired: true, - textFieldName: 'Unit Name', + isRequired: false, + textFieldName: 'Space Name', description: '', ), ), @@ -155,8 +152,7 @@ class AddDeviceDialog extends StatelessWidget { visitorBloc.deviceNameController.clear(); visitorBloc.deviceIdController.clear(); visitorBloc.unitNameController.clear(); - visitorBloc.add( - FetchDevice()); // Reset to original list + visitorBloc.add(FetchDevice()); // Reset to original list }, ), ), @@ -167,15 +163,16 @@ class AddDeviceDialog extends StatelessWidget { Expanded( flex: 3, child: state is TableLoaded - ? DynamicTable( + ? AccessDeviceTable( + uuidIndex: 1, + withSelectAll: true, initialSelectedIds: selectedDeviceIds, cellDecoration: containerDecoration, isEmpty: visitorBloc.data.isEmpty, selectAll: (p0) { visitorBloc.selectedDeviceIds.clear(); for (var item in state.data) { - visitorBloc - .add(SelectDeviceEvent(item.uuid)); + visitorBloc.add(SelectDeviceEvent(item.uuid)); } }, onRowSelected: (index, isSelected, row) { @@ -188,7 +185,7 @@ class AddDeviceDialog extends StatelessWidget { 'Device Name', 'Device ID', 'Access Type', - 'Unit Name', + 'Space Name', 'Status' ], data: state.data.map((item) { diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 20238252..1f014352 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -87,7 +87,8 @@ class VisitorPasswordDialog extends StatelessWidget { ], )) .then((v) { - Navigator.of(context).pop(); + Navigator.of(context).pop(true); + }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -211,28 +212,28 @@ class VisitorPasswordDialog extends StatelessWidget { }, ), ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Dynamic Password', - style: text, - ), - value: 'Dynamic Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - visitorBloc.usageFrequencySelected = ''; - } - }, - ), - ), + // SizedBox( + // width: size.width * 0.12, + // child: RadioListTile( + // contentPadding: EdgeInsets.zero, + // title: Text( + // 'Dynamic Password', + // style: text, + // ), + // value: 'Dynamic Password', + // groupValue: (state is PasswordTypeSelected) + // ? state.selectedType + // : visitorBloc.accessTypeSelected, + // onChanged: (String? value) { + // if (value != null) { + // context + // .read() + // .add(SelectPasswordType(value)); + // visitorBloc.usageFrequencySelected = ''; + // } + // }, + // ), + // ), ], )), const Spacer( @@ -256,14 +257,14 @@ class VisitorPasswordDialog extends StatelessWidget { color: ColorsManager.grayColor, fontSize: 9), ), - if (visitorBloc.accessTypeSelected == 'Dynamic Password') - Text( - 'Quick and short-acting password, only valid within 5 minutes after creation, the system randomly generates a digital password.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), - ), + // if (visitorBloc.accessTypeSelected == 'Dynamic Password') + // Text( + // 'Quick and short-acting password, only valid within 5 minutes after creation, the system randomly generates a digital password.', + // style: Theme.of(context).textTheme.bodySmall!.copyWith( + // fontWeight: FontWeight.w400, + // color: ColorsManager.grayColor, + // fontSize: 9), + // ), const SizedBox( height: 20, ) @@ -323,8 +324,7 @@ class VisitorPasswordDialog extends StatelessWidget { : visitorBloc.usageFrequencySelected, onChanged: (String? value) { if (value != null) { - context - .read() + context.read() .add(SelectUsageFrequency(value)); } }, @@ -344,7 +344,7 @@ class VisitorPasswordDialog extends StatelessWidget { if (visitorBloc.usageFrequencySelected == 'One-Time' && visitorBloc.accessTypeSelected == 'Offline Password') Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked.', + 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', style: Theme.of(context).textTheme.bodySmall!.copyWith( color: ColorsManager.grayColor, fontSize: 9), ), @@ -380,11 +380,9 @@ class VisitorPasswordDialog extends StatelessWidget { endTime: () { if (visitorBloc.usageFrequencySelected == 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add( - SelectTimeEvent(context: context, isEffective: false)); + visitorBloc.add(SelectTimeEvent(context: context, isEffective: false)); } else { - visitorBloc.add(SelectTimeVisitorPassword( - context: context, isStart: false, isRepeat: false)); + visitorBloc.add(SelectTimeVisitorPassword(context: context, isStart: false, isRepeat: false)); } }, startTime: () { @@ -398,16 +396,17 @@ class VisitorPasswordDialog extends StatelessWidget { } }, firstString: (visitorBloc.usageFrequencySelected == - 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') + 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') ? visitorBloc.effectiveTime : visitorBloc.startTimeAccess.toString(), secondString: (visitorBloc.usageFrequencySelected == - 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') + 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') ? visitorBloc.expirationTime : visitorBloc.endTimeAccess.toString(), icon: Assets.calendarIcon), + const SizedBox(height: 10,), + Text(visitorBloc.accessPeriodValidate, + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.red),), const SizedBox( height: 20, ), @@ -528,10 +527,20 @@ class VisitorPasswordDialog extends StatelessWidget { if (visitorBloc.usageFrequencySelected == 'One-Time' && visitorBloc.accessTypeSelected == 'Offline Password') { setPasswordFunction(context, size, visitorBloc); - } else if (visitorBloc.accessTypeSelected == 'Dynamic Password') { - print('objectobjectobjectobject'); - setPasswordFunction(context, size, visitorBloc); - } else { + } else if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Offline Password') { + if (visitorBloc.expirationTime != 'End Time' && + visitorBloc.effectiveTime != 'Start Time' ) { + setPasswordFunction(context, size, visitorBloc); + }else{ + visitorBloc.stateDialog( + context: context, + message: 'Please select Access Period to continue', + title: 'Access Period'); + } + } else if( + visitorBloc.endTimeAccess.toString()!='End Time' + &&visitorBloc.startTimeAccess.toString()!='Start Time') { if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { if (isRepeat == true) { @@ -555,6 +564,11 @@ class VisitorPasswordDialog extends StatelessWidget { message: 'Please select Access Period to continue', title: 'Access Period'); } + }else{ + visitorBloc.stateDialog( + context: context, + message: 'Please select Access Period to continue', + title: 'Access Period'); } } else { visitorBloc.stateDialog( @@ -564,57 +578,6 @@ class VisitorPasswordDialog extends StatelessWidget { } } }, - - // onPressed: () { - // if (visitorBloc.forgetFormKey.currentState!.validate()) { - // if (visitorBloc.selectedDevices.isNotEmpty) { - // switch (visitorBloc.usageFrequencySelected) { - // case 'One-Time': - // if (visitorBloc.accessTypeSelected == 'Offline Password') { - // setPasswordFunction(context, size, visitorBloc); - // } else { - // visitorBloc.stateDialog( - // context: context, - // message: 'Invalid combination of Access Type and Usage Frequency.', - // title: 'Error', - // ); - // } - // break; - // default: - // if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { - // if (isRepeat) { - // if (visitorBloc.expirationTime != 'End Time' && - // visitorBloc.effectiveTime != 'Start Time' && - // visitorBloc.selectedDays.isNotEmpty) { - // setPasswordFunction(context, size, visitorBloc); - // } else { - // visitorBloc.stateDialog( - // context: context, - // message: 'Please select days and fill start time and end time to continue', - // title: 'Access Period', - // ); - // } - // } else { - // setPasswordFunction(context, size, visitorBloc); - // } - // } else { - // visitorBloc.stateDialog( - // context: context, - // message: 'Please select Access Period to continue', - // title: 'Access Period', - // ); - // } - // break; - // } - // } else { - // visitorBloc.stateDialog( - // context: context, - // message: 'Please select devices to continue', - // title: 'Select Devices', - // ); - // } - // } - // }, borderRadius: 8, child: Text( 'Ok', @@ -724,39 +687,39 @@ class VisitorPasswordDialog extends StatelessWidget { borderRadius: 8, onPressed: () { Navigator.pop(context); - if (visitorBloc.accessTypeSelected == 'Dynamic Password') { - } else { - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') { - visitorBloc.add(OnlineOneTimePasswordEvent( - context: context, - passwordName: visitorBloc.userNameController.text, - email: visitorBloc.emailController.text, - )); - } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') { - visitorBloc.add(OnlineMultipleTimePasswordEvent( - passwordName: visitorBloc.userNameController.text, - email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), - )); - } else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add(OfflineOneTimePasswordEvent( - context: context, - passwordName: visitorBloc.userNameController.text, - email: visitorBloc.emailController.text, - )); - } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add(OfflineMultipleTimePasswordEvent( - passwordName: visitorBloc.userNameController.text, - email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), - )); - } + if (visitorBloc.usageFrequencySelected == 'One-Time' && + visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.add(OnlineOneTimePasswordEvent( + context: context, + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + )); + } + else if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.add(OnlineMultipleTimePasswordEvent( + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + )); + } + else if (visitorBloc.usageFrequencySelected == 'One-Time' && + visitorBloc.accessTypeSelected == 'Offline Password') { + visitorBloc.add(OfflineOneTimePasswordEvent( + context: context, + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + )); + } + else if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Offline Password') { + visitorBloc.add(OfflineMultipleTimePasswordEvent( + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + )); } }, child: Text( diff --git a/lib/services/access_mang_api.dart b/lib/services/access_mang_api.dart index c93466d4..103f6121 100644 --- a/lib/services/access_mang_api.dart +++ b/lib/services/access_mang_api.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:flutter/cupertino.dart'; import 'package:syncrow_web/pages/access_management/model/password_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart'; @@ -22,7 +21,6 @@ class AccessMangApi { ); return response; } catch (e) { - debugPrint('Error fetching visitor passwords: $e'); return []; } } @@ -42,7 +40,6 @@ class AccessMangApi { ); return response; } catch (e) { - debugPrint('Error fetching $e'); return []; } } @@ -123,13 +120,6 @@ class AccessMangApi { String? effectiveTime, String? invalidTime, List? devicesUuid}) async { - print(jsonEncode({ - "email": email, - "devicesUuid": devicesUuid, - "passwordName": passwordName, - "effectiveTime": effectiveTime, - "invalidTime": invalidTime, - })); final response = await HTTPService().post( path: ApiEndpoints.sendOffLineMultipleTime, body: jsonEncode({ diff --git a/lib/services/auth_api.dart b/lib/services/auth_api.dart index 6573f3ad..2b158cdb 100644 --- a/lib/services/auth_api.dart +++ b/lib/services/auth_api.dart @@ -1,5 +1,3 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/token.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -20,76 +18,40 @@ class AuthenticationAPI { static Future forgetPassword({ required var email, required var password, + required var otpCode, }) async { final response = await HTTPService().post( path: ApiEndpoints.forgetPassword, - body: {"email": email, "password": password}, + body: {"email": email, "password": password,"otpCode": otpCode}, showServerMessage: true, expectedResponseModel: (json) {}); return response; } static Future sendOtp({required String email, required String regionUuid}) async { - try { - final response = await HTTPService().post( - path: ApiEndpoints.sendOtp, - body: {"email": email, "type": "PASSWORD", "regionUuid": regionUuid}, - showServerMessage: true, - expectedResponseModel: (json) { - return json['data']['cooldown']; - }); - - return response; - } on DioException catch (e) { - if (e.response != null) { - if (e.response!.statusCode == 400) { - final errorData = e.response!.data; - String errorMessage = errorData['message']; - if (errorMessage == 'User not found') { - return 1; - } else { - int cooldown = errorData['data']['cooldown'] ?? 1; - return cooldown; - } - } else { - debugPrint('Error: ${e.response!.statusCode} - ${e.response!.statusMessage}'); - return 1; - } - } else { - debugPrint('Error: ${e.message}'); - return 1; - } - } catch (e) { - debugPrint('Unexpected Error: $e'); - return 1; - } + final response = await HTTPService().post( + path: ApiEndpoints.sendOtp, + body: {"email": email, "type": "PASSWORD", "regionUuid": regionUuid}, + showServerMessage: true, + expectedResponseModel: (json) { + return json['data']['cooldown']; + }); + return response; } static Future verifyOtp({required String email, required String otpCode}) async { - try { - final response = await HTTPService().post( - path: ApiEndpoints.verifyOtp, - body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) { - if (json['message'] == 'Otp Verified Successfully') { - return true; - } else { - return false; - } - }); - return response; - } on DioException catch (e) { - if (e.response != null) { - if (e.response!.statusCode == 400) { - final errorData = e.response!.data; - String errorMessage = errorData['message']; - return errorMessage; - } - } else { - debugPrint('Error: ${e.message}'); - } - } + final response = await HTTPService().post( + path: ApiEndpoints.verifyOtp, + body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) { + if (json['message'] == 'Otp Verified Successfully') { + return true; + } else { + return false; + } + }); + return response; } static Future> fetchRegion() async { diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 7e2dc4f8..5be1a9e4 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -1,7 +1,11 @@ +import 'dart:core'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -47,6 +51,22 @@ class DevicesManagementApi { } } + Future getPowerClampInfo(String deviceId) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.powerClamp.replaceAll('{powerClampUuid}', deviceId), + showServerMessage: true, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return {}; + } + } + //deviceControl Future deviceControl(String uuid, Status status) async { try { @@ -65,6 +85,31 @@ class DevicesManagementApi { } } + Future deviceBatchControl( + List uuids, String code, dynamic value) async { + try { + final body = { + 'devicesUuid': uuids, + 'code': code, + 'value': value, + }; + + final response = await HTTPService().post( + path: ApiEndpoints.deviceBatchControl, + body: body, + showServerMessage: true, + expectedResponseModel: (json) { + return (json['successResults'] as List).isNotEmpty; + }, + ); + + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + static Future> getDevicesByGatewayId( String gatewayId) async { final response = await HTTPService().get( @@ -95,7 +140,10 @@ class DevicesManagementApi { return response; } - static Future getDeviceReports(String uuid, String code) async { + static Future getDeviceReports( + String uuid, + String code, + ) async { final response = await HTTPService().get( path: ApiEndpoints.getDeviceLogs .replaceAll('{uuid}', uuid) @@ -107,4 +155,185 @@ class DevicesManagementApi { ); return response; } + + static Future getDeviceReportsByDate(String uuid, String code, + [String? from, String? to]) async { + final response = await HTTPService().get( + path: ApiEndpoints.getDeviceLogsByDate + .replaceAll('{uuid}', uuid) + .replaceAll('{code}', code) + .replaceAll('{startTime}', from ?? '') + .replaceAll('{endTime}', to ?? ''), + showServerMessage: false, + expectedResponseModel: (json) { + return DeviceReport.fromJson(json); + }, + ); + return response; + } + + Future getBatchStatus(List uuids) async { + try { + final queryParameters = { + 'devicesUuid': uuids.join(','), + }; + final response = await HTTPService().get( + path: ApiEndpoints.getBatchStatus, + queryParameters: queryParameters, + showServerMessage: true, + expectedResponseModel: (json) { + return DeviceStatus.fromJson(json['status']); + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return DeviceStatus( + productUuid: '', + productType: '', + status: [], + ); + } + } + + getPowerStatus(List uuids) async { + try { + final queryParameters = { + 'devicesUuid': uuids.join(','), + }; + final response = await HTTPService().get( + path: ApiEndpoints.getBatchStatus, + queryParameters: queryParameters, + showServerMessage: true, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return DeviceStatus( + productUuid: '', + productType: '', + status: [], + ); + } + } + + Future addScheduleRecord( + ScheduleEntry sendSchedule, String uuid) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), + body: sendSchedule.toMap(), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + + Future> getDeviceSchedules( + String uuid, String category) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.getScheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{category}', category), + showServerMessage: true, + expectedResponseModel: (json) { + List schedules = []; + for (var schedule in json) { + schedules.add(ScheduleModel.fromMap(schedule)); + } + return schedules; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return []; + } + } + + Future updateScheduleRecord( + {required bool enable, + required String uuid, + required String scheduleId}) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.updateScheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{scheduleUuid}', scheduleId), + body: { + 'scheduleId': scheduleId, + 'enable': enable, + }, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + + Future editScheduleRecord( + String uuid, ScheduleEntry newSchedule) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), + body: newSchedule.toMap(), + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + + Future deleteScheduleRecord(String uuid, String scheduleId) async { + try { + final response = await HTTPService().delete( + path: ApiEndpoints.deleteScheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{scheduleUuid}', scheduleId), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + + Future factoryReset(FactoryResetModel factoryReset, String uuid) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.factoryReset.replaceAll('{deviceUuid}', uuid), + body: factoryReset.toMap(), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 86510efc..6eb6a2d0 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -5,8 +5,7 @@ abstract class ColorsManager { static const Color switchOffColor = Color(0x7F8D99AE); static const Color primaryColor = Color(0xFF0030CB); //023DFE static const Color secondaryTextColor = Color(0xFF848484); - static Color primaryColorWithOpacity = - const Color(0xFF023DFE).withOpacity(0.6); + static Color primaryColorWithOpacity = const Color(0xFF023DFE).withOpacity(0.6); static const Color whiteColors = Colors.white; static const Color secondaryColor = Color(0xFF023DFE); static const Color onSecondaryColor = Color(0xFF023DFE); @@ -52,4 +51,5 @@ abstract class ColorsManager { static const Color spaceColor = Color(0xB2023DFE); static const Color counterBackgroundColor = Color(0xCCF4F4F4); } -//0036E6 \ No newline at end of file +//background: #background: #5D5D5D; + diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index a1bdd843..6198d7c1 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -1,69 +1,81 @@ -abstract class ApiEndpoints { - static const String baseUrl = 'http://localhost:4001'; - //static const String baseUrl = 'http://localhost:4001'; //Localhost +import 'package:flutter_dotenv/flutter_dotenv.dart'; -//https://syncrow-staging.azurewebsites.net -////////////////////////////////////// Authentication /////////////////////////////// - static const String signUp = '$baseUrl/authentication/user/signup'; - static const String login = '$baseUrl/authentication/user/login'; - static const String forgetPassword = - '$baseUrl/authentication/user/forget-password'; - static const String sendOtp = '$baseUrl/authentication/user/send-otp'; - static const String verifyOtp = '$baseUrl/authentication/user/verify-otp'; - static const String getRegion = '$baseUrl/region'; - static const String visitorPassword = '$baseUrl/visitor-password'; - static const String getDevices = '$baseUrl/visitor-password/devices'; +abstract class ApiEndpoints { + static String baseUrl = dotenv.env['BASE_URL'] ?? ''; + static const String signUp = '/authentication/user/signup'; + static const String login = '/authentication/user/login'; + static const String forgetPassword = '/authentication/user/forget-password'; + static const String sendOtp = '/authentication/user/send-otp'; + static const String verifyOtp = '/authentication/user/verify-otp'; + static const String getRegion = '/region'; + static const String visitorPassword = '/visitor-password'; + static const String getDevices = '/visitor-password/devices'; static const String sendOnlineOneTime = - '$baseUrl/visitor-password/temporary-password/online/one-time'; + '/visitor-password/temporary-password/online/one-time'; static const String sendOnlineMultipleTime = - '$baseUrl/visitor-password/temporary-password/online/multiple-time'; + '/visitor-password/temporary-password/online/multiple-time'; //offline Password static const String sendOffLineOneTime = - '$baseUrl/visitor-password/temporary-password/offline/one-time'; + '/visitor-password/temporary-password/offline/one-time'; static const String sendOffLineMultipleTime = - '$baseUrl/visitor-password/temporary-password/offline/multiple-time'; + '/visitor-password/temporary-password/offline/multiple-time'; - static const String getUser = '$baseUrl/user/{userUuid}'; + static const String getUser = '/user/{userUuid}'; ////// Devices Management //////////////// - static const String getAllDevices = '$baseUrl/device'; - static const String getDeviceStatus = - '$baseUrl/device/{uuid}/functions/status'; + static const String getAllDevices = '/device'; + static const String getDeviceStatus = '/device/{uuid}/functions/status'; + static const String getBatchStatus = '/device/status/batch'; - static const String deviceControl = '$baseUrl/device/{uuid}/control'; + static const String deviceControl = '/device/{uuid}/control'; + static const String deviceBatchControl = '/device/control/batch'; static const String gatewayApi = '/device/gateway/{gatewayUuid}/devices'; static const String openDoorLock = '/door-lock/open/{doorLockUuid}'; static const String getDeviceLogs = - '$baseUrl/device/report-logs/{uuid}?code={code}'; + '/device/report-logs/{uuid}?code={code}'; // Space Module static const String createSpace = - '$baseUrl/communities/{communityId}/spaces'; + '/communities/{communityId}/spaces'; static const String listSpaces = - '$baseUrl/communities/{communityId}/spaces'; + '/communities/{communityId}/spaces'; static const String deleteSpace = - '$baseUrl/communities/{communityId}/spaces/{spaceId}'; + '/communities/{communityId}/spaces/{spaceId}'; static const String updateSpace = - '$baseUrl/communities/{communityId}/spaces/{spaceId}'; + '/communities/{communityId}/spaces/{spaceId}'; static const String getSpace = - '$baseUrl/communities/{communityId}/spaces/{spaceId}'; + '/communities/{communityId}/spaces/{spaceId}'; static const String getSpaceHierarchy = - '$baseUrl/communities/{communityId}/spaces/hierarchy'; + '/communities/{communityId}/spaces/hierarchy'; // Community Module - static const String createCommunity = '$baseUrl/communities'; - static const String getCommunityList = '$baseUrl/communities'; + static const String createCommunity = '/communities'; + static const String getCommunityList = '/communities'; static const String getCommunityById = - '$baseUrl/communities/{communityId}'; + '/communities/{communityId}'; static const String updateCommunity = - '$baseUrl/communities/{communityId}'; + '/communities/{communityId}'; static const String deleteCommunity = - '$baseUrl/communities/{communityId}'; + '/communities/{communityId}'; static const String getUserCommunities = - '$baseUrl/communities/user/{userUuid}'; - static const String createUserCommunity = '$baseUrl/communities/user'; + '/communities/user/{userUuid}'; + static const String createUserCommunity = '/communities/user'; + static const String getDeviceLogsByDate = + '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; + + static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; + + static const String factoryReset = '/device/factory/reset/{deviceUuid}'; + static const String powerClamp = + '/device/{powerClampUuid}/power-clamp/status'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 5c605609..ed30fd74 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -13,9 +13,8 @@ class Assets { static const String rightLine = "assets/images/right_line.png"; static const String google = "assets/images/google.svg"; static const String facebook = "assets/images/facebook.svg"; - static const String invisiblePassword = - "assets/images/Password_invisible.svg"; - static const String visiblePassword = "assets/images/Password_visible.svg"; + static const String invisiblePassword = "assets/images/Password_invisible.svg"; + static const String visiblePassword = "assets/images/password_visible.svg"; static const String accessIcon = "assets/images/access_icon.svg"; static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; @@ -65,39 +64,23 @@ class Assets { static const String nobodyTime = "assets/icons/nobody_time.svg"; // Automation functions - static const String tempPasswordUnlock = - "assets/icons/automation_functions/temp_password_unlock.svg"; - static const String doorlockNormalOpen = - "assets/icons/automation_functions/doorlock_normal_open.svg"; - static const String doorbell = - "assets/icons/automation_functions/doorbell.svg"; - static const String remoteUnlockViaApp = - "assets/icons/automation_functions/remote_unlock_via_app.svg"; - static const String doubleLock = - "assets/icons/automation_functions/double_lock.svg"; - static const String selfTestResult = - "assets/icons/automation_functions/self_test_result.svg"; - static const String lockAlarm = - "assets/icons/automation_functions/lock_alarm.svg"; - static const String presenceState = - "assets/icons/automation_functions/presence_state.svg"; - static const String currentTemp = - "assets/icons/automation_functions/current_temp.svg"; - static const String presence = - "assets/icons/automation_functions/presence.svg"; - static const String residualElectricity = - "assets/icons/automation_functions/residual_electricity.svg"; - static const String hijackAlarm = - "assets/icons/automation_functions/hijack_alarm.svg"; - static const String passwordUnlock = - "assets/icons/automation_functions/password_unlock.svg"; - static const String remoteUnlockRequest = - "assets/icons/automation_functions/remote_unlock_req.svg"; - static const String cardUnlock = - "assets/icons/automation_functions/card_unlock.svg"; + static const String tempPasswordUnlock = "assets/icons/automation_functions/temp_password_unlock.svg"; + static const String doorlockNormalOpen = "assets/icons/automation_functions/doorlock_normal_open.svg"; + static const String doorbell = "assets/icons/automation_functions/doorbell.svg"; + static const String remoteUnlockViaApp = "assets/icons/automation_functions/remote_unlock_via_app.svg"; + static const String doubleLock = "assets/icons/automation_functions/double_lock.svg"; + static const String selfTestResult = "assets/icons/automation_functions/self_test_result.svg"; + static const String lockAlarm = "assets/icons/automation_functions/lock_alarm.svg"; + static const String presenceState = "assets/icons/automation_functions/presence_state.svg"; + static const String currentTemp = "assets/icons/automation_functions/current_temp.svg"; + static const String presence = "assets/icons/automation_functions/presence.svg"; + static const String residualElectricity = "assets/icons/automation_functions/residual_electricity.svg"; + static const String hijackAlarm = "assets/icons/automation_functions/hijack_alarm.svg"; + static const String passwordUnlock = "assets/icons/automation_functions/password_unlock.svg"; + static const String remoteUnlockRequest = "assets/icons/automation_functions/remote_unlock_req.svg"; + static const String cardUnlock = "assets/icons/automation_functions/card_unlock.svg"; static const String motion = "assets/icons/automation_functions/motion.svg"; - static const String fingerprintUnlock = - "assets/icons/automation_functions/fingerprint_unlock.svg"; + static const String fingerprintUnlock = "assets/icons/automation_functions/fingerprint_unlock.svg"; // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; @@ -172,4 +155,78 @@ class Assets { static const String Gang1SwitchIcon = 'assets/icons/1_Gang_switch_icon.svg'; static const String DoorLockIcon = 'assets/icons/door_lock.svg'; static const String SmartGatewayIcon = 'assets/icons/smart_gateway_icon.svg'; + static const String curtainIcon = "assets/images/curtain.svg"; + static const String unlock = 'assets/icons/unlock_ic.svg'; + static const String firmware = 'assets/icons/firmware.svg'; + //assets/images/scheduling.svg + static const String scheduling = 'assets/images/scheduling.svg'; + //assets/icons/main_door_notifi.svg + static const String mainDoorNotifi = 'assets/icons/main_door_notifi.svg'; + //assets/icons/main_door_reports.svg + static const String mainDoorReports = 'assets/icons/main_door_reports.svg'; + //assets/icons/main_door.svg + static const String mainDoor = 'assets/icons/main_door.svg'; + + //assets/icons/empty_records.svg + static const String emptyRecords = 'assets/icons/empty_records.svg'; + + //assets/icons/open_close_door.svg + static const String openCloseDoor = 'assets/icons/open_close_door.svg'; + + //assets/icons/open_close_records.svg + static const String openCloseRecords = 'assets/icons/open_close_records.svg'; + + //assets/icons/water_heater.svg + static const String waterHeater = 'assets/icons/water_heater.svg'; + + //assets/icons/ac_lock.svg + static const String acLock = 'assets/icons/ac_lock.svg'; + + //assets/icons/ac_schedule.svg + static const String acSchedule = 'assets/icons/ac_schedule.svg'; + + //assets/icons/preferences.svg + static const String preferences = 'assets/icons/preferences.svg'; + + static const String openedDoor = 'assets/icons/opened_door.svg'; + static const String closedDoor = 'assets/icons/closed_door.svg'; + static const String doorDelay = 'assets/icons/door_delay.svg'; + static const String records = 'assets/icons/records.svg'; + //assets/icons/water_leak_normal.svg + static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; + //assets/icons/water_leak_detected.svg + static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; + + //assets/icons/automation_records.svg + static const String automationRecords = 'assets/icons/automation_records.svg'; + + //assets/icons/1gang.svg + static const String oneGang = 'assets/icons/1gang.svg'; + + //assets/icons/2gang.svg + static const String twoGang = 'assets/icons/2gang.svg'; + + static const String frequencyIcon = "assets/icons/frequency_icon.svg"; + static const String voltMeterIcon = "assets/icons/volt_meter_icon.svg"; + static const String powerActiveIcon = "assets/icons/power_active_icon.svg"; + static const String searchIcon = "assets/icons/search_icon.svg"; + static const String voltageIcon = "assets/icons/voltage_icon.svg"; + static const String speedoMeter = "assets/icons/speedo_meter.svg"; + //assets/icons/account_setting.svg + static const String accountSetting = 'assets/icons/account_setting.svg'; + + //assets/icons/settings.svg + static const String settings = 'assets/icons/settings.svg'; + + //assets/icons/sign_out.svg + static const String signOut = 'assets/icons/sign_out.svg'; + + //assets/icons/logo_grey.svg + static const String logoGrey = 'assets/icons/logo-grey.svg'; + + //assets/icons/sos.svg + static const String sos = 'assets/icons/sos.svg'; + + //assets/icons/sos_normal.svg + static const String sosNormal = 'assets/icons/sos_normal.svg'; } diff --git a/lib/utils/constants/routes_const.dart b/lib/utils/constants/routes_const.dart index 622b090b..094787d4 100644 --- a/lib/utils/constants/routes_const.dart +++ b/lib/utils/constants/routes_const.dart @@ -1,5 +1,5 @@ class RoutesConst { - static const String auth = '/'; + static const String auth = '/auth'; static const String home = '/home'; static const String visitorPassword = '/visitor-password'; static const String accessManagementPage = '/access-management-page'; diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 94821ef6..2b1ce8a5 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -4,12 +4,37 @@ enum DeviceType { DoorLock, Curtain, Blind, + OneGang, + TwoGang, ThreeGang, Gateway, CeilingSensor, WallSensor, + WH, + DoorSensor, + GarageDoor, + WaterLeak, Other, } +/* + + 3G: + 1G: + 2G: + GW: + DL: + WPS: + CPS: + AC: + CUR: + WH: + DS: + 1GT: + 2GT: + 3GT: + GD: + WL: + */ Map devicesTypesMap = { "AC": DeviceType.AC, @@ -18,4 +43,14 @@ Map devicesTypesMap = { "DL": DeviceType.DoorLock, "WPS": DeviceType.WallSensor, "3G": DeviceType.ThreeGang, + "2G": DeviceType.TwoGang, + "1G": DeviceType.OneGang, + "CUR": DeviceType.Curtain, + "WH": DeviceType.WH, + 'DS': DeviceType.DoorSensor, + "1GT": DeviceType.OneGang, + "2GT": DeviceType.TwoGang, + "3GT": DeviceType.ThreeGang, + 'GD': DeviceType.GarageDoor, + 'WL': DeviceType.WaterLeak }; diff --git a/lib/utils/extension/build_context_x.dart b/lib/utils/extension/build_context_x.dart new file mode 100644 index 00000000..dbdbb347 --- /dev/null +++ b/lib/utils/extension/build_context_x.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +extension BuildContextExt on BuildContext { + ThemeData get theme => Theme.of(this); + + TextTheme get textTheme => Theme.of(this).textTheme; + + AppBarTheme get appBarTheme => Theme.of(this).appBarTheme; + + Size get screenSize => MediaQuery.of(this).size; + + double get screenWidth => MediaQuery.of(this).size.width; + + double get screenHeight => MediaQuery.of(this).size.height; + + double get textScale => MediaQuery.textScalerOf(this).scale(1); + + void customAlertDialog({ + required Widget alertBody, + required String title, + required VoidCallback onConfirm, + VoidCallback? onDismiss, + bool? hideConfirmButton, + final double? dialogWidth, + }) { + showDialog( + context: this, + builder: (BuildContext context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: dialogWidth ?? 360, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + /// header widget + Text( + title, + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 50, + ), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + + /// custom body content + Flexible(child: SingleChildScrollView(child: alertBody)), + + /// Footer buttons + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + hideConfirmButton != true + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: onDismiss ?? + () { + Navigator.pop(context); + }, + child: Center( + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium! + .copyWith(color: ColorsManager.greyColor), + ), + ), + ), + Container( + height: 50, + width: 1, + color: ColorsManager.greyColor, + ), + GestureDetector( + onTap: onConfirm, + child: Center( + child: Text( + 'Confirm', + style: context.textTheme.bodyMedium!.copyWith( + color: + ColorsManager.primaryColorWithOpacity), + ), + ), + ), + ], + ) + : Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: GestureDetector( + onTap: onDismiss ?? + () { + Navigator.pop(context); + }, + child: Center( + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium! + .copyWith(color: ColorsManager.greyColor), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/utils/format_date_time.dart b/lib/utils/format_date_time.dart index 5c089a2c..98d0eb5e 100644 --- a/lib/utils/format_date_time.dart +++ b/lib/utils/format_date_time.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; String formatDateTime(DateTime? dateTime) { @@ -9,3 +10,31 @@ String formatDateTime(DateTime? dateTime) { return '${dateFormatter.format(dateTime)} ${timeFormatter.format(dateTime)}'; } + +String formatTimeOfDayToISO(TimeOfDay time, {DateTime? currentDate}) { + final now = currentDate ?? DateTime.now(); + + final dateTime = DateTime( + now.year, + now.month, + now.day, + time.hour, + time.minute, + ); + // Convert DateTime to Unix timestamp (in seconds) + final unixTimestamp = dateTime.millisecondsSinceEpoch ~/ 1000; + + return unixTimestamp.toString(); +} + +String formatIsoStringToTime(String isoString, BuildContext context) { + try { + final parts = isoString.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + final timeOfDay = TimeOfDay(hour: hour, minute: minute); + return timeOfDay.format(context); + } catch (e) { + return isoString; + } +} diff --git a/lib/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart b/lib/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart index d64a5144..0d793aa6 100644 --- a/lib/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart +++ b/lib/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart @@ -2,15 +2,25 @@ import 'package:flutter/material.dart'; mixin HelperResponsiveLayout { bool isSmallScreenSize(BuildContext context) { - return MediaQuery.of(context).size.width < 700; + return MediaQuery.of(context).size.width < 600; } bool isMediumScreenSize(BuildContext context) { - return MediaQuery.of(context).size.width >= 700 && - MediaQuery.of(context).size.width < 1400; + return MediaQuery.of(context).size.width >= 600 && + MediaQuery.of(context).size.width < 1024; + } + + bool isHafMediumScreenSize(BuildContext context) { + return MediaQuery.of(context).size.width >= 600 / 1.3 && + MediaQuery.of(context).size.width < 1024 / 1.3; } bool isLargeScreenSize(BuildContext context) { - return MediaQuery.of(context).size.width >= 1400; + return MediaQuery.of(context).size.width >= 1024 && + MediaQuery.of(context).size.width < 1440; + } + + bool isExtraLargeScreenSize(BuildContext context) { + return MediaQuery.of(context).size.width >= 1440; } } diff --git a/lib/utils/style.dart b/lib/utils/style.dart index f4e84b50..12b72521 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -33,10 +33,10 @@ InputDecoration? textBoxDecoration({bool suffixIcon = false}) => BoxDecoration containerDecoration = BoxDecoration( boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 5, - blurRadius: 8, - offset: const Offset(0, 3), // changes position of shadow + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow ), ], color: ColorsManager.boxColor, diff --git a/lib/utils/theme/theme.dart b/lib/utils/theme/theme.dart new file mode 100644 index 00000000..5ac61afa --- /dev/null +++ b/lib/utils/theme/theme.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +final myTheme = ThemeData( + fontFamily: 'Aftika', + useMaterial3: true, + textTheme: const TextTheme( + bodySmall: TextStyle( + fontSize: 13, + color: ColorsManager.whiteColors, + fontWeight: FontWeight.normal, + ), + bodyMedium: TextStyle(color: Colors.black87, fontSize: 14), + bodyLarge: TextStyle(fontSize: 16, color: Colors.white), + headlineSmall: TextStyle(color: Colors.black87, fontSize: 18), + headlineMedium: TextStyle(color: Colors.black87, fontSize: 20), + headlineLarge: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + colorScheme: ColorScheme.fromSeed( + seedColor: ColorsManager.blueColor, + primary: ColorsManager.blueColor, + onSurface: Colors.grey.shade400, + ), + switchTheme: SwitchThemeData( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return ColorsManager.blueColor; + } + return ColorsManager.whiteColors; + }), + trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return ColorsManager.blueColor.withOpacity(0.5); + } + return ColorsManager.whiteColors; + }), + ), + checkboxTheme: CheckboxThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return ColorsManager.blueColor; + } + return Colors.grey.shade200; + }), + checkColor: WidgetStateProperty.all(Colors.white), + side: const BorderSide(color: ColorsManager.whiteColors), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), +); diff --git a/lib/utils/user_drop_down_menu.dart b/lib/utils/user_drop_down_menu.dart new file mode 100644 index 00000000..3a0c4194 --- /dev/null +++ b/lib/utils/user_drop_down_menu.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/constants/routes_const.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class UserDropdownMenu extends StatefulWidget { + const UserDropdownMenu({super.key, required this.user}); + final UserModel? user; + + @override + _UserDropdownMenuState createState() => _UserDropdownMenuState(); +} + +class _UserDropdownMenuState extends State { + bool _isDropdownOpen = false; + + void _toggleDropdown() { + setState(() { + _isDropdownOpen = !_isDropdownOpen; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () async { + _toggleDropdown(); + await _showPopupMenu(context); + setState(() { + _isDropdownOpen = false; + }); + }, + child: Transform.rotate( + angle: _isDropdownOpen ? -1.5708 : 1.5708, + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 16, + ), + ), + ), + ], + ); + } + + Future _showPopupMenu(BuildContext context) async { + final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + overlay.size.width, + 75, + 0, + overlay.size.height, + ), + Offset.zero & overlay.size, + ); + + await showMenu( + context: context, + position: position, + color: ColorsManager.whiteColors, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + items: [ + PopupMenuItem( + onTap: () {}, + child: ListTile( + leading: SvgPicture.asset(Assets.accountSetting), + title: Text( + "Account Settings", + style: context.textTheme.bodyMedium, + ), + ), + ), + PopupMenuItem( + onTap: () {}, + child: ListTile( + leading: SvgPicture.asset(Assets.settings), + title: Text( + "Settings", + style: context.textTheme.bodyMedium, + ), + ), + ), + PopupMenuItem( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + final size = MediaQuery.of(context).size; + return AlertDialog( + alignment: Alignment.center, + content: SizedBox( + height: 200, + width: 400, + child: Padding( + padding: const EdgeInsets.only(top: 24, left: 24, right: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + Assets.blackLogo, + height: 40, + width: 200, + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + 'Log out of your Syncrow account', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + SizedBox.square( + dimension: 80, + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + child: SizedBox.square( + dimension: 78, + child: SvgPicture.asset( + Assets.logoGrey, + fit: BoxFit.fitHeight, + height: 80, + ), + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${widget.user?.firstName ?? ''} ${widget.user?.lastName}', + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + Text( + ' ${widget.user?.email}', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + actionsAlignment: MainAxisAlignment.center, + actions: [ + SizedBox( + width: 200, + child: GestureDetector( + onTap: () { + context.pop(); + }, + child: DefaultButton( + backgroundColor: ColorsManager.boxColor, + elevation: 1, + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 12, + color: Colors.black, + ), + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + GestureDetector( + onTap: () { + AuthBloc.logout(); + context.go(RoutesConst.auth); + }, + child: SizedBox( + width: 200, + child: DefaultButton( + elevation: 1, + child: Text( + 'Logout', + style: + Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 12, color: Colors.white), + ), + ), + ), + ), + ]); + }, + ); + }, + child: ListTile( + leading: SvgPicture.asset(Assets.signOut), + title: Text( + "Log Out", + style: context.textTheme.bodyMedium, + ), + ), + ), + ], + ).then((value) { + setState(() { + _isDropdownOpen = false; + }); + }); + } +} diff --git a/lib/web_layout/web_app_bar.dart b/lib/web_layout/web_app_bar.dart index 6ad54d0b..777b0931 100644 --- a/lib/web_layout/web_app_bar.dart +++ b/lib/web_layout/web_app_bar.dart @@ -1,65 +1,192 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; +import 'package:syncrow_web/utils/user_drop_down_menu.dart'; -class WebAppBar extends StatelessWidget { +class WebAppBar extends StatefulWidget { final Widget? title; - final List? body; - const WebAppBar({super.key, this.title, this.body}); + final Widget? centerBody; + final Widget? rightBody; + + const WebAppBar({super.key, this.title, this.centerBody, this.rightBody}); + + @override + State createState() => _WebAppBarState(); +} + +class _WebAppBarState extends State with HelperResponsiveLayout { + @override + void initState() { + super.initState(); + } @override Widget build(BuildContext context) { + bool isSmallScreen = isSmallScreenSize(context); + bool isHalfMediumScreen = isHafMediumScreenSize(context); return BlocBuilder(builder: (context, state) { + final user = context.read().user; return Container( - height: 100, + height: (isSmallScreen || isHalfMediumScreen) ? 130 : 100, decoration: const BoxDecoration(color: ColorsManager.secondaryColor), padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: title!, - ), - if (body != null) - Expanded( - flex: 2, - child: Wrap( - spacing: 15, // Adjust the spacing as needed - children: body!, - ), - ), - Row( - children: [ - const SizedBox( - width: 10, - ), - const SizedBox.square( - dimension: 40, - child: CircleAvatar( - backgroundColor: Colors.white, - child: SizedBox.square( - dimension: 35, - child: CircleAvatar( - backgroundColor: Colors.grey, - child: FlutterLogo(), - ), + child: isSmallScreen || isHalfMediumScreen + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title != null) + Align( + alignment: Alignment.centerLeft, + child: widget.title!, + ), + if (widget.centerBody != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: widget.centerBody, + ), + if (widget.rightBody != null || user != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.rightBody != null) widget.rightBody!, + Row( + children: [ + SizedBox.square( + dimension: 40, + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + child: SizedBox.square( + dimension: 35, + child: SvgPicture.asset( + Assets.logoGrey, + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + if (user != null) + Text( + '${user.firstName} ${user.lastName}', + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ], + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Row( + children: [ + widget.title!, + if (widget.centerBody != null) + Padding( + padding: const EdgeInsets.only(left: 80), + child: widget.centerBody!, + ), + ], ), ), - ), - const SizedBox( - width: 10, - ), - if (HomeBloc.user != null) - Text( - '${HomeBloc.user!.firstName.toString()} ${HomeBloc.user!.lastName.toString()} ', - style: Theme.of(context).textTheme.bodyLarge, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.rightBody != null) + Align( + alignment: Alignment.centerRight, + child: widget.rightBody, + ), + const SizedBox( + width: 10, + ), + SizedBox.square( + dimension: 40, + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + child: SizedBox.square( + dimension: 35, + child: SvgPicture.asset( + Assets.logoGrey, + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + if (user != null) + Text( + '${user.firstName} ${user.lastName}', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox( + width: 10, + ), + UserDropdownMenu(user: user), + // GestureDetector( + // onTap: () { + // showCustomDialog( + // context: context, + // barrierDismissible: true, + // title: 'Logout', + // message: 'Are you sure you want to logout?', + // actions: [ + // GestureDetector( + // onTap: () { + // AuthBloc.logout(); + // context.go(RoutesConst.auth); + // }, + // child: DefaultButton( + // child: Text( + // 'Ok', + // style: Theme.of(context) + // .textTheme + // .bodyMedium! + // .copyWith(fontSize: 12, color: Colors.white), + // ), + // ), + // ), + // const SizedBox( + // height: 10, + // ), + // GestureDetector( + // onTap: () { + // context.pop(); + // }, + // child: DefaultButton( + // child: Text( + // 'Cancel', + // style: Theme.of(context) + // .textTheme + // .bodyMedium! + // .copyWith(fontSize: 12, color: Colors.white), + // ), + // ), + // ), + // ], + // ); + // }, + // child: const Icon( + // Icons.logout, + // color: ColorsManager.whiteColors, + // ), + // ) + ], ), - ], - ) - ], - ), + ], + ), ); }); } diff --git a/lib/web_layout/web_scaffold.dart b/lib/web_layout/web_scaffold.dart index 72bcb777..c1d6075f 100644 --- a/lib/web_layout/web_scaffold.dart +++ b/lib/web_layout/web_scaffold.dart @@ -6,16 +6,19 @@ import 'package:syncrow_web/web_layout/web_app_bar.dart'; import 'menu_sidebar.dart'; class WebScaffold extends StatelessWidget with HelperResponsiveLayout { - final bool enableMenuSideba; + final bool enableMenuSidebar; final Widget? appBarTitle; - final List? appBarBody; + final Widget? centerBody; + final Widget? rightBody; final Widget? scaffoldBody; - const WebScaffold( - {super.key, - this.appBarTitle, - this.appBarBody, - this.scaffoldBody, - this.enableMenuSideba = true}); + const WebScaffold({ + super.key, + this.appBarTitle, + this.centerBody, + this.rightBody, + this.scaffoldBody, + this.enableMenuSidebar = false, + }); @override Widget build(BuildContext context) { final isSmall = isSmallScreenSize(context); @@ -40,12 +43,13 @@ class WebScaffold extends StatelessWidget with HelperResponsiveLayout { opacity: 0.7, child: WebAppBar( title: appBarTitle, - body: appBarBody, + centerBody: centerBody, + rightBody: rightBody, )), Expanded( child: Row( children: [ - if (enableMenuSideba && !isSmall) const MenuSidebar(), + if (enableMenuSidebar && !isSmall) const MenuSidebar(), Expanded(flex: 5, child: scaffoldBody!) ], ), diff --git a/pubspec.lock b/pubspec.lock index 7973e388..92a76b10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + dropdown_button2: + dependency: "direct main" + description: + name: dropdown_button2 + sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1 + url: "https://pub.dev" + source: hosted + version: "2.3.9" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab" + url: "https://pub.dev" + source: hosted + version: "5.0.6" equatable: dependency: "direct main" description: @@ -121,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" + url: "https://pub.dev" + source: hosted + version: "0.69.0" flutter: dependency: "direct main" description: flutter @@ -134,6 +158,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.5" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -585,10 +617,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a8e96a9b..7742e4da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: syncrow_web -description: "A new Flutter project." +description: "Smart Home Solutions" # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -43,9 +43,13 @@ dependencies: get_it: ^7.6.7 flutter_secure_storage: ^9.2.2 shared_preferences: ^2.3.0 + dropdown_button2: ^2.3.9 data_table_2: ^2.5.15 go_router: intl: ^0.19.0 + dropdown_search: ^5.0.6 + flutter_dotenv: ^5.1.0 + fl_chart: ^0.69.0 dev_dependencies: flutter_test: @@ -75,6 +79,9 @@ flutter: - assets/icons/ - assets/images/ - assets/ + - .env.development + - .env.staging + - .env.production # An image asset can refer to one or more resolution-specific "variants", see diff --git a/web/favicon.png b/web/favicon.png index 8aaa46ac..7bf7ebbf 100644 Binary files a/web/favicon.png and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index b749bfef..5170aa47 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 88cfd48d..c58953df 100644 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d76..5170aa47 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index d69c5669..c58953df 100644 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html index b3fc8dfc..a8b2aa25 100644 --- a/web/index.html +++ b/web/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/manifest.json b/web/manifest.json index dc91e8ad..b037b66b 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -5,7 +5,7 @@ "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "A new Flutter project.", + "description": "Smart Home Solutions", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [