diff --git a/assets/icons/close_settings_icon.svg b/assets/icons/close_settings_icon.svg new file mode 100644 index 00000000..93e615d8 --- /dev/null +++ b/assets/icons/close_settings_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/edit_name_icon_settings.svg b/assets/icons/edit_name_icon_settings.svg new file mode 100644 index 00000000..54bee0af --- /dev/null +++ b/assets/icons/edit_name_icon_settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 60abc0d2..a40faeb3 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -21,6 +21,7 @@ class DynamicTable extends StatefulWidget { final List? initialSelectedIds; final int uuidIndex; final Function(dynamic selectedRows)? onSelectionChanged; + final Function(int rowIndex)? onSettingsPressed; const DynamicTable({ super.key, required this.headers, @@ -37,6 +38,7 @@ class DynamicTable extends StatefulWidget { this.initialSelectedIds, required this.uuidIndex, this.onSelectionChanged, + this.onSettingsPressed, }); @override @@ -63,7 +65,8 @@ class _DynamicTableState extends State { } } - bool _compareListOfLists(List> oldList, List> newList) { + bool _compareListOfLists( + List> oldList, List> newList) { // Check if the old and new lists are the same if (oldList.length != newList.length) return false; @@ -132,7 +135,8 @@ class _DynamicTableState extends State { children: [ if (widget.withCheckBox) _buildSelectAllCheckbox(), ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell(widget.headers[index], index); + return _buildTableHeaderCell( + widget.headers[index], index); }) //...widget.headers.map((header) => _buildTableHeaderCell(header)), ], @@ -153,11 +157,14 @@ class _DynamicTableState extends State { height: 15, ), Text( - widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices', + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', style: Theme.of(context) .textTheme .bodySmall! - .copyWith(color: ColorsManager.grayColor), + .copyWith( + color: ColorsManager.grayColor), ) ], ), @@ -166,16 +173,28 @@ class _DynamicTableState extends State { ], ) : Column( - children: List.generate(widget.data.length, (index) { - final row = widget.data[index]; + children: + List.generate(widget.data.length, (rowIndex) { + final row = widget.data[rowIndex]; return Row( children: [ - if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08), - ...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)), + if (widget.withCheckBox) + _buildRowCheckbox( + rowIndex, widget.size.height * 0.08), + ...row.asMap().entries.map((entry) { + int columnIndex = entry.key; + dynamic cell = entry.value; + return _buildTableCell( + cell.toString(), + widget.size.height * 0.08, + rowIndex: rowIndex, + columnIndex: columnIndex, + ); + }).toList(), ], ); }), - ), + ) ], ), ), @@ -196,7 +215,9 @@ class _DynamicTableState extends State { ), child: Checkbox( value: _selectAll, - onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, + onChanged: widget.withSelectAll && widget.data.isNotEmpty + ? _toggleSelectAll + : null, ), ); } @@ -238,7 +259,9 @@ class _DynamicTableState extends State { constraints: const BoxConstraints.expand(height: 40), alignment: Alignment.centerLeft, child: Padding( - padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), + padding: EdgeInsets.symmetric( + horizontal: index == widget.headers.length - 1 ? 12 : 8.0, + vertical: 4), child: Text( title, style: context.textTheme.titleSmall!.copyWith( @@ -253,13 +276,23 @@ class _DynamicTableState extends State { ); } - Widget _buildTableCell(String content, double size) { + Widget _buildTableCell( + String content, + double size, { + required int rowIndex, + required int columnIndex, + }) { bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; if (isBatteryLevel) { batteryLevel = double.tryParse(content.replaceAll('%', '').trim()); } + bool isSettingsColumn = widget.headers[columnIndex] == 'Settings'; + + if (isSettingsColumn) { + return _buildSettingsIcon(rowIndex, size); + } Color? statusColor; switch (content) { @@ -311,4 +344,22 @@ class _DynamicTableState extends State { ), ); } + + Widget _buildSettingsIcon(int rowIndex, double size) { + return Container( + height: size, + padding: const EdgeInsets.all(5.0), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: ColorsManager.boxDivider, width: 1.0), + ), + color: Colors.white, + ), + alignment: Alignment.center, + child: IconButton( + icon: SvgPicture.asset(Assets.settings), + onPressed: () => widget.onSettingsPressed?.call(rowIndex), + ), + ); + } } 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 fd3a2574..755bc8b7 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 @@ -95,7 +95,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { return const RoutinesView(); } if (state.createRoutineView) { - return CreateNewRoutineView(); + return const CreateNewRoutineView(); } return BlocBuilder( 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 a3c975c1..f4baad0c 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 @@ -6,9 +6,11 @@ import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_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/all_devices/widgets/device_search_filters.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_settings_panel.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/pages/space_tree/view/space_tree_view.dart'; +import 'package:syncrow_web/utils/color_manager.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'; @@ -58,7 +60,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Low Battery ($lowBatteryCount)', ]; - final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + final buttonLabel = + (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; return Row( children: [ @@ -105,18 +108,23 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { if (selectedDevices.length == 1) { showDialog( context: context, - builder: (context) => DeviceControlDialog( + builder: (context) => + DeviceControlDialog( device: selectedDevices.first, ), ); - } else if (selectedDevices.length > 1) { - final productTypes = selectedDevices - .map((device) => device.productType) - .toSet(); + } else if (selectedDevices.length > + 1) { + final productTypes = + selectedDevices + .map((device) => + device.productType) + .toSet(); if (productTypes.length == 1) { showDialog( context: context, - builder: (context) => DeviceBatchControlDialog( + builder: (context) => + DeviceBatchControlDialog( devices: selectedDevices, ), ); @@ -130,7 +138,9 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: isControlButtonEnabled ? Colors.white : Colors.grey, + color: isControlButtonEnabled + ? Colors.white + : Colors.grey, ), ), ), @@ -166,29 +176,40 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Installation Date and Time', 'Status', 'Last Offline Date and Time', + 'Settings' ], data: devicesToShow.map((device) { final combinedSpaceNames = device.spaces != null - ? device.spaces!.map((space) => space.spaceName).join(' > ') + + ? device.spaces! + .map((space) => space.spaceName) + .join(' > ') + (device.community != null ? ' > ${device.community!.name}' : '') - : (device.community != null ? device.community!.name : ''); + : (device.community != null + ? device.community!.name + : ''); return [ device.name ?? '', device.productName ?? '', device.uuid ?? '', - (device.spaces != null && device.spaces!.isNotEmpty) + (device.spaces != null && + device.spaces!.isNotEmpty) ? device.spaces![0].spaceName : '', combinedSpaceNames, - device.batteryLevel != null ? '${device.batteryLevel}%' : '-', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.createTime ?? 0) * 1000)), + device.batteryLevel != null + ? '${device.batteryLevel}%' + : '-', + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.updateTime ?? 0) * 1000)), + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.updateTime ?? 0) * 1000)), + 'Settings', ]; }).toList(), onSelectionChanged: (selectedRows) { @@ -202,6 +223,10 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .map((device) => device.uuid!) .toList(), isEmpty: devicesToShow.isEmpty, + onSettingsPressed: (rowIndex) { + final device = devicesToShow[rowIndex]; + showDeviceSettingsSidebar(context, device); + }, ), ), ) @@ -213,4 +238,37 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { }, ); } + + void showDeviceSettingsSidebar(BuildContext context, AllDevicesModel device) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: "Device Settings", + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, anim1, anim2) { + return Align( + alignment: Alignment.centerRight, + child: Material( + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.whiteColors, + child: DeviceSettingsPanel( + device: device, + onClose: () => Navigator.of(context).pop(), + ), + ), + ), + ); + }, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(anim1), + child: child, + ); + }, + ); + } } diff --git a/lib/pages/device_managment/device_setting/bloc/device_info_model.dart b/lib/pages/device_managment/device_setting/bloc/device_info_model.dart new file mode 100644 index 00000000..65a48508 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/device_info_model.dart @@ -0,0 +1,182 @@ +class DeviceInfoModel { + final int activeTime; + final String category; + final String categoryName; + final int createTime; + final String gatewayId; + final String icon; + final String ip; + final String lat; + final String localKey; + final String lon; + final String model; + final String name; + final String nodeId; + final bool online; + final String ownerId; + final String productName; + final bool sub; + final String timeZone; + final int updateTime; + final String uuid; + final String productUuid; + final String productType; + final String permissionType; + final String macAddress; + final Subspace subspace; + + DeviceInfoModel({ + required this.activeTime, + required this.category, + required this.categoryName, + required this.createTime, + required this.gatewayId, + required this.icon, + required this.ip, + required this.lat, + required this.localKey, + required this.lon, + required this.model, + required this.name, + required this.nodeId, + required this.online, + required this.ownerId, + required this.productName, + required this.sub, + required this.timeZone, + required this.updateTime, + required this.uuid, + required this.productUuid, + required this.productType, + required this.permissionType, + required this.macAddress, + required this.subspace, + }); + + factory DeviceInfoModel.fromJson(Map json) { + return DeviceInfoModel( + activeTime: json['activeTime'], + category: json['category'], + categoryName: json['categoryName'], + createTime: json['createTime'], + gatewayId: json['gatewayId'], + icon: json['icon'], + ip: json['ip'] ?? "", + lat: json['lat'], + localKey: json['localKey'], + lon: json['lon'], + model: json['model'], + name: json['name'], + nodeId: json['nodeId'], + online: json['online'], + ownerId: json['ownerId'], + productName: json['productName'], + sub: json['sub'], + timeZone: json['timeZone'], + updateTime: json['updateTime'], + uuid: json['uuid'], + productUuid: json['productUuid'], + productType: json['productType'], + permissionType: json['permissionType'] ?? '', + macAddress: json['macAddress'], + subspace: Subspace.fromJson(json['subspace']), + ); + } + + Map toJson() { + return { + 'activeTime': activeTime, + 'category': category, + 'categoryName': categoryName, + 'createTime': createTime, + 'gatewayId': gatewayId, + 'icon': icon, + 'ip': ip, + 'lat': lat, + 'localKey': localKey, + 'lon': lon, + 'model': model, + 'name': name, + 'nodeId': nodeId, + 'online': online, + 'ownerId': ownerId, + 'productName': productName, + 'sub': sub, + 'timeZone': timeZone, + 'updateTime': updateTime, + 'uuid': uuid, + 'productUuid': productUuid, + 'productType': productType, + 'permissionType': permissionType, + 'macAddress': macAddress, + 'subspace': subspace.toJson(), + }; + } + + static DeviceInfoModel empty() { + return DeviceInfoModel( + activeTime: 0, + category: '', + categoryName: '', + createTime: 0, + gatewayId: '', + icon: '', + ip: '', + lat: '', + localKey: '', + lon: '', + model: '', + name: '', + nodeId: '', + online: false, + ownerId: '', + productName: '', + sub: false, + timeZone: '', + updateTime: 0, + uuid: '', + productUuid: '', + productType: '', + permissionType: '', + macAddress: '', + subspace: Subspace( + uuid: '', + createdAt: '', + updatedAt: '', + subspaceName: '', + ), + ); + } +} + +class Subspace { + final String uuid; + final String createdAt; + final String updatedAt; + final String subspaceName; + + Subspace({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.subspaceName, + }); + + factory Subspace.fromJson(Map json) { + return Subspace( + uuid: json['uuid'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + subspaceName: json['subspaceName'], + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'subspaceName': subspaceName, + }; + } +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart new file mode 100644 index 00000000..55e5e74e --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -0,0 +1,149 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; +part 'setting_bloc_event.dart'; + +class SettingBlocBloc extends Bloc { + final String deviceId; + SettingBlocBloc({ + required this.deviceId, + }) : super(const SettingBlocInitial()) { + on(fetchDeviceInfo); + on(saveName); + on(_changeName); + on(deleteDevice); + //on(_fetchRoomsAndDevices); + } + static String deviceName = ''; + final TextEditingController nameController = + TextEditingController(text: deviceName); + List roomsList = []; + bool isEditingName = false; + + bool _validateInputs() { + final nameError = fullNameValidator(nameController.text); + if (nameError != null) { + CustomSnackBar.displaySnackBar(nameError); + return true; + } + return false; + } + + String? fullNameValidator(String? value) { + if (value == null) return 'name is required'; + final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); + if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { + return 'name must be between 2 and 30 characters long'; + } + if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) { + return 'Only alphanumeric characters, space, dash and single quote are allowed'; + } + return null; + } + + Future saveName( + SaveNameEvent event, Emitter emit) async { + if (_validateInputs()) return; + try { + emit(SettingLoadingState()); + var response = await DevicesManagementApi.putDeviceName( + deviceId: deviceId, deviceName: nameController.text); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(UpdateSettingState(deviceName: nameController.text)); + } catch (e) { + emit(ErrorState(message: e.toString())); + } finally { + // isSaving = false; + } + } + + DeviceInfoModel deviceInfo = DeviceInfoModel( + activeTime: 0, + category: "", + categoryName: "", + createTime: 0, + gatewayId: "", + icon: "", + ip: "", + lat: "", + localKey: "", + lon: "", + model: "", + name: "", + nodeId: "", + online: false, + ownerId: "", + productName: "", + sub: false, + timeZone: "", + updateTime: 0, + uuid: "", + productUuid: "", + productType: "", + permissionType: "", + macAddress: "", + subspace: Subspace( + uuid: "", + createdAt: "", + updatedAt: "", + subspaceName: "", + ), + ); + + Future fetchDeviceInfo( + DeviceSettingInitialInfo event, Emitter emit) async { + try { + emit(SettingLoadingState()); + var response = await DevicesManagementApi.getDeviceInfo(deviceId); + deviceInfo = DeviceInfoModel.fromJson(response); + nameController.text = deviceInfo.name; + + emit(UpdateSettingState( + deviceName: nameController.text, + deviceInfo: deviceInfo, + )); + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } + + bool editName = false; + final FocusNode focusNode = FocusNode(); + + void _changeName(ChangeNameEvent event, Emitter emit) { + emit(SettingLoadingState()); + editName = event.value!; + if (editName) { + Future.delayed(const Duration(milliseconds: 500), () { + focusNode.requestFocus(); + }); + } else { + add(const SaveNameEvent()); + focusNode.unfocus(); + } + emit(UpdateSettingState(deviceName: deviceName, deviceInfo: deviceInfo)); + } + + void deleteDevice( + DeleteDeviceEvent event, Emitter emit) async { + try { + emit(SettingLoadingState()); + var response = + await DevicesManagementApi.resetDevise(devicesUuid: deviceId); + CustomSnackBar.displaySnackBar('Reset Successfully'); + emit(UpdateSettingState( + deviceName: nameController.text, + deviceInfo: deviceInfo, + )); + } catch (e) { + emit(ErrorState(message: e.toString())); + return; + } + } +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart new file mode 100644 index 00000000..737c8889 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -0,0 +1,50 @@ +part of 'setting_bloc_bloc.dart'; + +abstract class SettingBlocEvent extends Equatable { + const SettingBlocEvent(); + @override + List get props => []; +} + +class SaveDeviceName extends SettingBlocEvent { + final String deviceName; + final String deviceId; + + const SaveDeviceName({required this.deviceName, required this.deviceId}); + + @override + List get props => [deviceName, deviceId]; +} + +class StartEditingName extends SettingBlocEvent {} + +class CancelEditingName extends SettingBlocEvent {} + +class ChangeEditingNameValue extends SettingBlocEvent { + final String value; + const ChangeEditingNameValue(this.value); + + @override + List get props => [value]; +} + +class FetchRoomsEvent extends SettingBlocEvent { + final String deviceId; + + const FetchRoomsEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class SaveNameEvent extends SettingBlocEvent { + const SaveNameEvent(); +} + +class DeviceSettingInitialInfo extends SettingBlocEvent {} + +class ChangeNameEvent extends SettingBlocEvent { + final bool? value; + const ChangeNameEvent({this.value}); +} +class DeleteDeviceEvent extends SettingBlocEvent {} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart new file mode 100644 index 00000000..65907c67 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; + +abstract class DeviceSettingsState extends Equatable { + const DeviceSettingsState(); + + @override + List get props => []; +} + +class SettingBlocInitial extends DeviceSettingsState { + final String deviceName; + final String deviceId; + final bool isEditingName; + final String editingNameValue; + + const SettingBlocInitial({ + this.deviceName = '', + this.deviceId = '', + this.isEditingName = false, + this.editingNameValue = '', + }); + + SettingBlocInitial copyWith({ + String? deviceName, + String? deviceId, + bool? isEditingName, + String? editingNameValue, + }) => + SettingBlocInitial( + deviceName: deviceName ?? this.deviceName, + deviceId: deviceId ?? this.deviceId, + isEditingName: isEditingName ?? this.isEditingName, + editingNameValue: editingNameValue ?? this.editingNameValue, + ); + + @override + List get props => + [deviceName, deviceId, isEditingName, editingNameValue]; +} + +class SettingLoadingState extends DeviceSettingsState {} + +class UpdateSettingState extends DeviceSettingsState { + final String deviceName; + final DeviceInfoModel? deviceInfo; + const UpdateSettingState({required this.deviceName, this.deviceInfo}); + + @override + List get props => [deviceName, deviceInfo]; +} + +class ErrorState extends DeviceSettingsState { + final String message; + + const ErrorState({required this.message}); + @override + List get props => [message]; +} + +class FetchRoomsState extends DeviceSettingsState { + final List roomsList; + + const FetchRoomsState({required this.roomsList}); + + @override + List get props => [roomsList]; +} diff --git a/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart b/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart new file mode 100644 index 00000000..bc68b33e --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart @@ -0,0 +1,35 @@ +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; + +class SubSpaceModel { + final String? id; + final String? name; + List? devices; + + SubSpaceModel({ + required this.id, + required this.name, + required this.devices, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'devices': devices?.map((device) => device.toJson()).toList(), + }; + } + + factory SubSpaceModel.fromJson(Map json) { + List devices = []; + if (json['devices'] != null) { + for (var device in json['devices']) { + devices.add(DeviceModel.fromJson(device)); + } + } + return SubSpaceModel( + id: json['uuid'], + name: json['subspaceName'], + devices: devices, + ); + } +} diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart new file mode 100644 index 00000000..2415ab90 --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.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/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.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/web_layout/default_container.dart'; + +class DeviceSettingsPanel extends StatelessWidget { + final VoidCallback? onClose; + final AllDevicesModel device; + + const DeviceSettingsPanel({this.onClose, super.key, required this.device}); + + @override + Widget build(BuildContext context) { + final sectionTitle = context.theme.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ); + Widget infoRow( + {required String label, required String value, Widget? trailing}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.grayColor, + ), + ), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + trailing ?? const SizedBox.shrink(), + ], + ), + ); + } + + return BlocProvider( + create: (context) => SettingBlocBloc( + deviceId: device.uuid ?? '', + )..add(DeviceSettingInitialInfo()), + child: BlocBuilder( + builder: (context, state) { + final iconPath = + DeviceTypeHelper.getDeviceIconByTypeCode(device.productType); + final _bloc = BlocProvider.of(context); + DeviceInfoModel deviceInfo = DeviceInfoModel.empty(); + if (state is UpdateSettingState) { + deviceInfo = state.deviceInfo!; + } + return Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.grey25, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: ListView( + children: [ + /// Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: SvgPicture.asset(Assets.closeSettingsIcon), + onPressed: onClose ?? () => Navigator.of(context).pop(), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Device Settings', + style: context.theme.textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.primaryColor)), + ], + ), + const SizedBox(height: 24), + + /// Device Name + Icon + DefaultContainer( + child: Row( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: + const Color.fromARGB(177, 213, 213, 213), + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + radius: 36, + child: SvgPicture.asset( + iconPath, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + maxLength: 30, + style: const TextStyle( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.center, + focusNode: _bloc.focusNode, + controller: _bloc.nameController, + enabled: _bloc.editName, + onFieldSubmitted: (value) { + _bloc.add(const ChangeNameEvent(value: false)); + }, + decoration: const InputDecoration( + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), + ), + ), + const SizedBox(width: 8), + _bloc.editName == true + ? const SizedBox() + : GestureDetector( + onTap: () { + _bloc.add(const ChangeNameEvent(value: true)); + }, + child: SvgPicture.asset( + Assets.editNameIconSettings, + color: ColorsManager.grayColor, + height: 20, + width: 20, + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + /// Device Management + Text('Device Management', style: sectionTitle), + DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + children: [ + const SizedBox( + height: 5, + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Sub-Space:', + value: device.subspace!.subspaceName, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Virtual Address:', + value: deviceInfo.productUuid, + trailing: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: device.productUuid ?? ''), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Virtual Address copied to clipboard'), + ), + ); + }, + child: const Icon( + Icons.copy, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'MAC Address:', + value: deviceInfo.macAddress), + ), + const SizedBox( + height: 5, + ), + ], + ), + ), + const SizedBox(height: 32), + + /// Remove Device Button + SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + _bloc.add(DeleteDeviceEvent()); + }, + child: const DefaultContainer( + padding: EdgeInsets.all(25), + child: Center( + child: Text( + 'Remove Device', + style: TextStyle(color: ColorsManager.red), + ), + ), + ), + ), + ) + ], + ), + ); + })); + } +} + +class DeviceTypeHelper { + static const Map _iconMap = { + 'AC': Assets.ac, + 'GW': Assets.gateway, + 'CPS': Assets.sensors, + 'DL': Assets.doorLock, + 'WPS': Assets.sensors, + '3G': Assets.gangSwitch, + '2G': Assets.twoGang, + '1G': Assets.oneGang, + 'CUR': Assets.curtain, + 'WH': Assets.waterHeater, + 'DS': Assets.doorSensor, + '1GT': Assets.oneTouchSwitch, + '2GT': Assets.twoTouchSwitch, + '3GT': Assets.threeTouchSwitch, + 'GD': Assets.garageDoor, + 'WL': Assets.waterLeakNormal, + 'NCPS': Assets.sensors, + }; + + static String getDeviceIconByTypeCode(String? typeCode) { + if (typeCode == null) return Assets.logoHorizontal; + return _iconMap[typeCode] ?? Assets.logoHorizontal; + } +} diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index b4de6326..97ac95d8 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -91,7 +91,8 @@ class DevicesManagementApi { } } - Future deviceBatchControl(List uuids, String code, dynamic value) async { + Future deviceBatchControl( + List uuids, String code, dynamic value) async { try { final body = { 'devicesUuid': uuids, @@ -116,7 +117,8 @@ class DevicesManagementApi { } } - static Future> getDevicesByGatewayId(String gatewayId) async { + static Future> getDevicesByGatewayId( + String gatewayId) async { final response = await HTTPService().get( path: ApiEndpoints.gatewayApi.replaceAll('{gatewayUuid}', gatewayId), showServerMessage: false, @@ -150,7 +152,9 @@ class DevicesManagementApi { String code, ) async { final response = await HTTPService().get( - path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code), + path: ApiEndpoints.getDeviceLogs + .replaceAll('{uuid}', uuid) + .replaceAll('{code}', code), showServerMessage: false, expectedResponseModel: (json) { return DeviceReport.fromJson(json['data']); @@ -223,7 +227,8 @@ class DevicesManagementApi { } } - Future addScheduleRecord(ScheduleEntry sendSchedule, String uuid) async { + Future addScheduleRecord( + ScheduleEntry sendSchedule, String uuid) async { try { final response = await HTTPService().post( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -240,7 +245,8 @@ class DevicesManagementApi { } } - Future> getDeviceSchedules(String uuid, String category) async { + Future> getDeviceSchedules( + String uuid, String category) async { try { final response = await HTTPService().get( path: ApiEndpoints.getScheduleByDeviceId @@ -263,7 +269,9 @@ class DevicesManagementApi { } Future updateScheduleRecord( - {required bool enable, required String uuid, required String scheduleId}) async { + {required bool enable, + required String uuid, + required String scheduleId}) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateScheduleByDeviceId @@ -284,7 +292,8 @@ class DevicesManagementApi { } } - Future editScheduleRecord(String uuid, ScheduleEntry newSchedule) async { + Future editScheduleRecord( + String uuid, ScheduleEntry newSchedule) async { try { final response = await HTTPService().put( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -335,4 +344,46 @@ class DevicesManagementApi { return false; } } + + static Future> putDeviceName( + {required String deviceId, required String deviceName}) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + body: {"deviceName": deviceName}, + expectedResponseModel: (json) { + return json['data']; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future getDeviceInfo(String deviceId) async { + final response = await HTTPService().get( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + expectedResponseModel: (json) { + return json['data'] as Map; + }); + return response; + } + static Future resetDevise({ + String? devicesUuid, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.resetDevice.replaceAll('{deviceUuid}', devicesUuid!), + showServerMessage: false, + body: { + "devicesUuid": [devicesUuid] + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } + } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 19e219b6..048c7b40 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; @@ -12,14 +13,16 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class CommunitySpaceManagementApi { // Community Management APIs - Future> fetchCommunities(String projectId, {int page = 1}) async { + Future> fetchCommunities(String projectId, + {int page = 1}) async { try { List allCommunities = []; bool hasNext = true; while (hasNext) { await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + path: ApiEndpoints.getCommunityList + .replaceAll('{projectId}', projectId), queryParameters: { 'page': page, }, @@ -55,8 +58,14 @@ class CommunitySpaceManagementApi { try { bool hasNext = false; await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), - queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search}, + path: + ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + queryParameters: { + 'page': page, + 'includeSpaces': true, + 'size': 25, + 'search': search + }, expectedResponseModel: (json) { try { List jsonData = json['data'] ?? []; @@ -68,7 +77,10 @@ class CommunitySpaceManagementApi { page = currentPage + 1; paginationModel = PaginationModel( - pageNum: page, hasNext: hasNext, size: 25, communities: communityList); + pageNum: page, + hasNext: hasNext, + size: 25, + communities: communityList); return paginationModel; } catch (_) { hasNext = false; @@ -83,7 +95,8 @@ class CommunitySpaceManagementApi { Future getCommunityById(String communityId) async { try { final response = await HTTPService().get( - path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), + path: ApiEndpoints.getCommunityById + .replaceAll('{communityId}', communityId), expectedResponseModel: (json) { return CommunityModel.fromJson(json['data']); }, @@ -95,7 +108,8 @@ class CommunitySpaceManagementApi { } } - Future createCommunity(String name, String description, String projectId) async { + Future createCommunity( + String name, String description, String projectId) async { try { final response = await HTTPService().post( path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId), @@ -114,7 +128,8 @@ class CommunitySpaceManagementApi { } } - Future updateCommunity(String communityId, String name, String projectId) async { + Future updateCommunity( + String communityId, String name, String projectId) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateCommunity @@ -151,7 +166,8 @@ class CommunitySpaceManagementApi { } } - Future fetchSpaces(String communityId, String projectId) async { + Future fetchSpaces( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.listSpaces @@ -177,7 +193,8 @@ class CommunitySpaceManagementApi { } } - Future getSpace(String communityId, String spaceId, String projectId) async { + Future getSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpace @@ -289,7 +306,8 @@ class CommunitySpaceManagementApi { } } - Future deleteSpace(String communityId, String spaceId, String projectId) async { + Future deleteSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().delete( path: ApiEndpoints.deleteSpace @@ -307,15 +325,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceHierarchy(String communityId, String projectId) async { + Future> getSpaceHierarchy( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpaceHierarchy .replaceAll('{communityId}', communityId) .replaceAll('{projectId}', projectId), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, @@ -327,15 +347,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceOnlyWithDevices({String? communityId, String? projectId}) async { + Future> getSpaceOnlyWithDevices( + {String? communityId, String? projectId}) async { try { final response = await HTTPService().get( path: ApiEndpoints.spaceOnlyWithDevices .replaceAll('{communityId}', communityId!) .replaceAll('{projectId}', projectId!), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, ); @@ -345,4 +367,36 @@ class CommunitySpaceManagementApi { return []; } } + + static Future> getSubSpaceBySpaceId( + String communityId, String spaceId, String projectId) async { + try { + // Construct the API path + final path = ApiEndpoints.listSubspace + .replaceFirst('{communityUuid}', communityId) + .replaceFirst('{spaceUuid}', spaceId) + .replaceAll('{projectUuid}', projectId); + + final response = await HTTPService().get( + path: path, + queryParameters: {"page": 1, "pageSize": 10}, + showServerMessage: false, + expectedResponseModel: (json) { + List rooms = []; + if (json['data'] != null) { + for (var subspace in json['data']) { + rooms.add(SubSpaceModel.fromJson(subspace)); + } + } else { + print("Warning: 'data' key is missing or null in response JSON."); + } + return rooms; + }, + ); + + return response; + } catch (error, stackTrace) { + return []; // Return an empty list if there's an error + } + } } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 5a892aa6..4062ff7e 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -73,4 +73,6 @@ abstract class ColorsManager { static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); + static const Color grey25 = Color(0xFFF9F9F9); + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 454ec46d..472055bd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -60,9 +60,12 @@ abstract class ApiEndpoints { '/devices/{uuid}/report-logs?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 getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/devices/batch'; //product @@ -124,4 +127,10 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{unitUuid}/automations'; static const String spaceOnlyWithDevices = '/projects/{projectId}/communities/{communityId}/spaces?onlyWithDevices=true'; + + static const String listSubspace = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces'; + static const String deviceByUuid = '/devices/{deviceUuid}'; + + static const String resetDevice = '/factory/reset/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index f857a357..d5d5beca 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -481,4 +481,9 @@ class Assets { static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg'; static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg'; static const String blankCalendar = 'assets/icons/blank_calendar.svg'; + static const String closeSettingsIcon = + 'assets/icons/close_settings_icon.svg'; + + static const String editNameIconSettings = + 'assets/icons/edit_name_icon_settings.svg'; } diff --git a/lib/web_layout/default_container.dart b/lib/web_layout/default_container.dart new file mode 100644 index 00000000..e0a71b04 --- /dev/null +++ b/lib/web_layout/default_container.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class DefaultContainer extends StatelessWidget { + const DefaultContainer({ + super.key, + required this.child, + this.height, + this.width, + this.color, + this.boxConstraints, + this.margin, + this.padding, + this.onTap, + this.borderRadius, + }); + + final double? height; + final double? width; + final Widget child; + final BoxConstraints? boxConstraints; + final EdgeInsets? margin; + final EdgeInsets? padding; + final Color? color; + final Function()? onTap; + final BorderRadius? borderRadius; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + height: height, + width: width, + margin: margin ?? const EdgeInsets.only(right: 3, bottom: 3), + constraints: boxConstraints, + decoration: BoxDecoration( + color: color ?? Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(20), + ), + padding: padding ?? const EdgeInsets.all(10), + child: child, + ), + ); + } +}