diff --git a/assets/icons/1G_touch_switch.svg b/assets/icons/1G_touch_switch.svg new file mode 100644 index 00000000..45679cfb --- /dev/null +++ b/assets/icons/1G_touch_switch.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/1_Gang_switch_icon.svg b/assets/icons/1_Gang_switch_icon.svg new file mode 100644 index 00000000..33a0755b --- /dev/null +++ b/assets/icons/1_Gang_switch_icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/2G_touch_switch.svg b/assets/icons/2G_touch_switch.svg new file mode 100644 index 00000000..1893fd21 --- /dev/null +++ b/assets/icons/2G_touch_switch.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/2_Gang_Switch_icon.svg b/assets/icons/2_Gang_Switch_icon.svg new file mode 100644 index 00000000..e72fa4f7 --- /dev/null +++ b/assets/icons/2_Gang_Switch_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/3G_touch_switch.svg b/assets/icons/3G_touch_switch.svg new file mode 100644 index 00000000..4a271a4a --- /dev/null +++ b/assets/icons/3G_touch_switch.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/3_Gang_switch_icon.svg b/assets/icons/3_Gang_switch_icon.svg new file mode 100644 index 00000000..45dea511 --- /dev/null +++ b/assets/icons/3_Gang_switch_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/add_icon.svg b/assets/icons/add_icon.svg new file mode 100644 index 00000000..e31d09ac --- /dev/null +++ b/assets/icons/add_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/bbq_icon.svg b/assets/icons/bbq_icon.svg new file mode 100644 index 00000000..00fb5639 --- /dev/null +++ b/assets/icons/bbq_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/building_icon.svg b/assets/icons/building_icon.svg new file mode 100644 index 00000000..60d32f23 --- /dev/null +++ b/assets/icons/building_icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg new file mode 100644 index 00000000..050a4521 --- /dev/null +++ b/assets/icons/delete.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/desk_icon.png b/assets/icons/desk_icon.png new file mode 100644 index 00000000..33b6dbb3 Binary files /dev/null and b/assets/icons/desk_icon.png differ diff --git a/assets/icons/desk_icon.svg b/assets/icons/desk_icon.svg new file mode 100644 index 00000000..04b35926 --- /dev/null +++ b/assets/icons/desk_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/door_icon.svg b/assets/icons/door_icon.svg new file mode 100644 index 00000000..7d7f48e2 --- /dev/null +++ b/assets/icons/door_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/door_lock.svg b/assets/icons/door_lock.svg new file mode 100644 index 00000000..2302d58d --- /dev/null +++ b/assets/icons/door_lock.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/door_sensor.svg b/assets/icons/door_sensor.svg new file mode 100644 index 00000000..fdeb661c --- /dev/null +++ b/assets/icons/door_sensor.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg new file mode 100644 index 00000000..ac510f4a --- /dev/null +++ b/assets/icons/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/garage_opener.svg b/assets/icons/garage_opener.svg new file mode 100644 index 00000000..20a31aca --- /dev/null +++ b/assets/icons/garage_opener.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/gym_icon.svg b/assets/icons/gym_icon.svg new file mode 100644 index 00000000..cd98149f --- /dev/null +++ b/assets/icons/gym_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon_edit_icon.svg b/assets/icons/icon_edit_icon.svg new file mode 100644 index 00000000..39e1a5c8 --- /dev/null +++ b/assets/icons/icon_edit_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/location_icon.svg b/assets/icons/location_icon.svg new file mode 100644 index 00000000..f4ffd535 --- /dev/null +++ b/assets/icons/location_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/parking_icon.svg b/assets/icons/parking_icon.svg new file mode 100644 index 00000000..353c3245 --- /dev/null +++ b/assets/icons/parking_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/pool_icon.svg b/assets/icons/pool_icon.svg new file mode 100644 index 00000000..72097e3a --- /dev/null +++ b/assets/icons/pool_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/power_clamp.svg b/assets/icons/power_clamp.svg new file mode 100644 index 00000000..a2f21e3a --- /dev/null +++ b/assets/icons/power_clamp.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/presence_sensor.svg b/assets/icons/presence_sensor.svg new file mode 100644 index 00000000..f1bdfb90 --- /dev/null +++ b/assets/icons/presence_sensor.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/rounded_add_icon.svg b/assets/icons/rounded_add_icon.svg new file mode 100644 index 00000000..35068a99 --- /dev/null +++ b/assets/icons/rounded_add_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/sauna_icon.svg b/assets/icons/sauna_icon.svg new file mode 100644 index 00000000..62c77438 --- /dev/null +++ b/assets/icons/sauna_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/smart_gateway_icon.svg b/assets/icons/smart_gateway_icon.svg new file mode 100644 index 00000000..33207c58 --- /dev/null +++ b/assets/icons/smart_gateway_icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/smart_light_icon.svg b/assets/icons/smart_light_icon.svg new file mode 100644 index 00000000..a0013940 --- /dev/null +++ b/assets/icons/smart_light_icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/smart_thermostat_icon.svg b/assets/icons/smart_thermostat_icon.svg new file mode 100644 index 00000000..d5782750 --- /dev/null +++ b/assets/icons/smart_thermostat_icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/stair_icon.svg b/assets/icons/stair_icon.svg new file mode 100644 index 00000000..a4690bd7 --- /dev/null +++ b/assets/icons/stair_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/steam_room_icon.svg b/assets/icons/steam_room_icon.svg new file mode 100644 index 00000000..a967f997 --- /dev/null +++ b/assets/icons/steam_room_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/street_icon.svg b/assets/icons/street_icon.svg new file mode 100644 index 00000000..f0b3d969 --- /dev/null +++ b/assets/icons/street_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/textfield_search_icon.svg b/assets/icons/textfield_search_icon.svg new file mode 100644 index 00000000..143af01c --- /dev/null +++ b/assets/icons/textfield_search_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/unit_icon.svg b/assets/icons/unit_icon.svg new file mode 100644 index 00000000..a62232f0 --- /dev/null +++ b/assets/icons/unit_icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/villa_icon.svg b/assets/icons/villa_icon.svg new file mode 100644 index 00000000..edc6b6e1 --- /dev/null +++ b/assets/icons/villa_icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/water_leak_sensor.svg b/assets/icons/water_leak_sensor.svg new file mode 100644 index 00000000..8f67d0ee --- /dev/null +++ b/assets/icons/water_leak_sensor.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/common/custom_expansion_tile.dart b/lib/common/custom_expansion_tile.dart new file mode 100644 index 00000000..8df9b663 --- /dev/null +++ b/lib/common/custom_expansion_tile.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CustomExpansionTile extends StatefulWidget { + final String title; + final List? children; + final bool initiallyExpanded; + final bool isSelected; // Add this to track selection + final bool? isExpanded; // External control over expansion + final ValueChanged? onExpansionChanged; // Notify when expansion changes + final VoidCallback? onItemSelected; // Callback for selecting the item + + CustomExpansionTile({ + required this.title, + this.children, + this.initiallyExpanded = false, + this.isExpanded, // Allow external control over expansion + this.onExpansionChanged, // Notify when expansion changes + this.onItemSelected, // Trigger item selection when name is tapped + required this.isSelected, // Add this to initialize selection state + }); + + @override + CustomExpansionTileState createState() => CustomExpansionTileState(); +} + +class CustomExpansionTileState extends State { + bool _isExpanded = false; // Local expansion state + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + void didUpdateWidget(CustomExpansionTile oldWidget) { + super.didUpdateWidget(oldWidget); + // Sync local state with external control of expansion state + if (widget.isExpanded != null && widget.isExpanded != _isExpanded) { + setState(() { + _isExpanded = widget.isExpanded!; + }); + } + } + + // Utility function to capitalize the first letter of the title + String _capitalizeFirstLetter(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + // Checkbox with independent state management + Checkbox( + value: false, + onChanged: (bool? value) { + setState(() {}); + }, + side: WidgetStateBorderSide.resolveWith((states) { + return const BorderSide(color: ColorsManager.grayBorder); + }), + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return ColorsManager.grayBorder; + } else { + return ColorsManager.checkBoxFillColor; + } + }), + checkColor: ColorsManager.whiteColors, + ), + // Expand/collapse icon, now wrapped in a GestureDetector for specific onTap + if (widget.children != null && widget.children!.isNotEmpty) + GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; + widget.onExpansionChanged?.call(_isExpanded); + }); + }, + child: Icon( + _isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, + color: ColorsManager.lightGrayColor, + size: 16.0, // Adjusted size for better alignment + ), + ), + // The title text, wrapped in GestureDetector to handle selection + Expanded( + child: GestureDetector( + onTap: () { + if (widget.onItemSelected != null) { + widget.onItemSelected!(); + } + }, + child: Text( + _capitalizeFirstLetter(widget.title), + style: TextStyle( + color: widget.isSelected + ? ColorsManager.blackColor // Change color to black when selected + : ColorsManager.lightGrayColor, // Gray when not selected + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ], + ), + // The expanded section (children) that shows when the tile is expanded + if (_isExpanded && widget.children != null && widget.children!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 48.0), // Indented children + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.children!, + ), + ), + ], + ); + } +} diff --git a/lib/common/search_bar.dart b/lib/common/search_bar.dart new file mode 100644 index 00000000..728fad33 --- /dev/null +++ b/lib/common/search_bar.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CustomSearchBar extends StatelessWidget { + final TextEditingController? controller; + final String hintText; + final Function(String)? onSearchChanged; // Callback for search input changes + + const CustomSearchBar({ + super.key, + this.controller, + this.hintText = 'Search', + this.onSearchChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 0, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + child: TextField( + controller: controller, + style: const TextStyle( + color: Colors.black, + ), + onChanged: onSearchChanged, // Call the callback on text change + decoration: InputDecoration( + filled: true, + fillColor: ColorsManager.textFieldGreyColor, + hintText: hintText, + hintStyle: TextStyle( + color: Color(0xB2999999), + fontSize: 12, + fontFamily: 'Aftika', + fontWeight: FontWeight.w400, + height: 0, + letterSpacing: -0.24, + ), + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 16), + child: SvgPicture.asset( + Assets.textFieldSearch, + width: 24, + height: 24, + ), + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/common/buttons/add_space_button.dart b/lib/pages/common/buttons/add_space_button.dart new file mode 100644 index 00000000..28de2af9 --- /dev/null +++ b/lib/pages/common/buttons/add_space_button.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AddSpaceButton extends StatelessWidget { + final VoidCallback onTap; + + const AddSpaceButton({super.key, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, // Handle tap event + child: Container( + width: 120, // Width of the button + height: 60, // Height of the button + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), // Rounded corners + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), // Shadow color + spreadRadius: 5, // Spread radius of the shadow + blurRadius: 7, // Blur effect + offset: const Offset(0, 3), // Shadow position + ), + ], + ), + child: Center( + child: Container( + width: 40, // Size of the inner circle + height: 40, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, // Light gray background + shape: BoxShape.circle, // Circular shape for the icon container + ), + child: const Icon( + Icons.add, // Add icon + color: Colors.blue, // Icon color + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/common/buttons/cancel_button.dart b/lib/pages/common/buttons/cancel_button.dart new file mode 100644 index 00000000..da6dcdc7 --- /dev/null +++ b/lib/pages/common/buttons/cancel_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CancelButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final double? height; // Optional height parameter for customization + final double? borderRadius; // Optional border radius customization + final double? width; + + const CancelButton({ + super.key, + required this.label, // Button label + required this.onPressed, // Button action + this.height = 40, // Default height + this.width = 140, + this.borderRadius = 10, // Default border radius + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(ColorsManager.boxColor), // White background + foregroundColor: WidgetStateProperty.all(Colors.black), // Black text color + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 10), + side: const BorderSide(color: ColorsManager.boxColor), // Black border + ), + ), + fixedSize: WidgetStateProperty.all(Size(width ?? 50, height ?? 40)), // Set button height + ), + child: Text(label), // Dynamic label + ); + } +} \ No newline at end of file diff --git a/lib/pages/common/buttons/default_button.dart b/lib/pages/common/buttons/default_button.dart index 8c391ecb..4aa748b7 100644 --- a/lib/pages/common/buttons/default_button.dart +++ b/lib/pages/common/buttons/default_button.dart @@ -15,7 +15,7 @@ class DefaultButton extends StatelessWidget { this.backgroundColor, this.foregroundColor, this.borderRadius, - this.height, + this.height = 40, this.padding, this.borderColor, this.elevation, @@ -70,14 +70,14 @@ class DefaultButton extends StatelessWidget { borderRadius: BorderRadius.circular(borderRadius ?? 20), ), ), - fixedSize: WidgetStateProperty.all( - const Size.fromHeight(50), - ), + fixedSize: height != null + ? WidgetStateProperty.all(Size.fromHeight(height!)) + : null, padding: WidgetStateProperty.all( EdgeInsets.all(padding ?? 10), ), minimumSize: WidgetStateProperty.all( - const Size.fromHeight(50), + const Size.fromHeight(10), ), elevation: WidgetStateProperty.all(elevation ?? 0), ), diff --git a/lib/pages/device_managment/all_devices/models/device_type_model.dart b/lib/pages/device_managment/all_devices/models/device_type_model.dart new file mode 100644 index 00000000..da228d3b --- /dev/null +++ b/lib/pages/device_managment/all_devices/models/device_type_model.dart @@ -0,0 +1,22 @@ +class DeviceTypeModel { + final String name; + final String icon; + + DeviceTypeModel({required this.name, required this.icon}); + + // Factory method for creating a new DeviceTypeModel from JSON + factory DeviceTypeModel.fromJson(Map json) { + return DeviceTypeModel( + name: json['name'], + icon: json['icon'], + ); + } + + // Convert this model to JSON format + Map toJson() { + return { + 'name': name, + 'icon': icon, + }; + } +} diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index 98320a88..32f3a5c0 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -74,7 +74,9 @@ class HomeBloc extends Bloc { title: 'Space Management', icon: Assets.spaseManagementIcon, active: true, - onPress: (context) {}, + onPress: (context) { + context.go(RoutesConst.spacesManagementPage); + }, color: ColorsManager.primaryColor, ), HomeItemModel( diff --git a/lib/pages/spaces_management/bloc/space_management_bloc.dart b/lib/pages/spaces_management/bloc/space_management_bloc.dart new file mode 100644 index 00000000..f178dd3a --- /dev/null +++ b/lib/pages/spaces_management/bloc/space_management_bloc.dart @@ -0,0 +1,259 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_state.dart'; +import 'package:syncrow_web/services/product_api.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; + +class SpaceManagementBloc extends Bloc { + final CommunitySpaceManagementApi _api; + final ProductApi _productApi; + + List? _cachedProducts; + + SpaceManagementBloc(this._api, this._productApi) : super(SpaceManagementInitial()) { + on(_onLoadCommunityAndSpaces); + on(_onUpdateSpacePosition); + on(_onCreateCommunity); + on(_onSaveSpaces); + on(_onFetchProducts); + on(_onCommunityDelete); + on(_onUpdateCommunity); + } + + void _onUpdateCommunity( + UpdateCommunityEvent event, + Emitter emit, + ) async { + final previousState = state; + try { + emit(SpaceManagementLoading()); + final success = await _api.updateCommunity(event.communityUuid, event.name); + if (success) { + if (previousState is SpaceManagementLoaded) { + final updatedCommunities = List.from(previousState.communities); + for(var community in updatedCommunities){ + if(community.uuid == event.communityUuid){ + community.name = event.name; + break; + } + } + emit(SpaceManagementLoaded( + communities: updatedCommunities, + products: previousState.products, + selectedCommunity: previousState.selectedCommunity, + )); + + } + } else { + emit(const SpaceManagementError('Failed to update the community.')); + } + } catch (e) { + emit(SpaceManagementError('Error updating community: $e')); + } + } + + void _onFetchProducts( + FetchProductsEvent event, + Emitter emit, + ) async { + if (_cachedProducts != null) { + // Products are already cached, no need to fetch again + return; + } + + try { + final products = await _productApi.fetchProducts(); + _cachedProducts = products; // Cache the products locally + } catch (e) { + emit(SpaceManagementError('Error fetching products: $e')); + } + } + + void _onLoadCommunityAndSpaces( + LoadCommunityAndSpacesEvent event, + Emitter emit, + ) async { + emit(SpaceManagementLoading()); + try { + if (_cachedProducts == null) { + final products = await _productApi.fetchProducts(); + _cachedProducts = products; + } + + // Fetch all communities + List communities = await _api.fetchCommunities(); + + List updatedCommunities = await Future.wait( + communities.map((community) async { + List spaces = await _api.getSpaceHierarchy(community.uuid); + return CommunityModel( + uuid: community.uuid, + createdAt: community.createdAt, + updatedAt: community.updatedAt, + name: community.name, + description: community.description, + spaces: spaces, // New spaces list + region: community.region, + ); + }).toList(), + ); + + emit(SpaceManagementLoaded(communities: updatedCommunities, products: _cachedProducts ?? [])); + } catch (e) { + emit(SpaceManagementError('Error loading communities and spaces: $e')); + } + } + + void _onCommunityDelete( + DeleteCommunityEvent event, + Emitter emit, + ) async { + try { + emit(SpaceManagementLoading()); + + final success = await _api.deleteCommunity(event.communityUuid); + if (success) { + add(LoadCommunityAndSpacesEvent()); + } else { + emit(const SpaceManagementError('Failed to delete the community.')); + } + } catch (e) { + // Handle unexpected errors + emit(SpaceManagementError('Error saving spaces: $e')); + } + } + + void _onUpdateSpacePosition( + UpdateSpacePositionEvent event, + Emitter emit, + ) {} + + void _onCreateCommunity( + CreateCommunityEvent event, + Emitter emit, + ) async { + final previousState = state; + emit(SpaceManagementLoading()); + + try { + CommunityModel? newCommunity = await _api.createCommunity(event.name, event.description); + + if (newCommunity != null) { + if (previousState is SpaceManagementLoaded) { + final updatedCommunities = List.from(previousState.communities) + ..add(newCommunity); + emit(SpaceManagementLoaded( + communities: updatedCommunities, + products: _cachedProducts ?? [], + selectedCommunity: newCommunity)); + } + } else { + emit(const SpaceManagementError('Error creating community')); + } + } catch (e) { + emit(SpaceManagementError('Error creating community: $e')); + } + } + + void _onSaveSpaces( + SaveSpacesEvent event, + Emitter emit, + ) async { + final previousState = state; + emit(SpaceManagementLoading()); + + try { + final updatedSpaces = await saveSpacesHierarchically(event.spaces, event.communityUuid); + emit(SpaceCreationSuccess(spaces: updatedSpaces)); + add(LoadCommunityAndSpacesEvent()); + } catch (e) { + emit(SpaceManagementError('Error saving spaces: $e')); + if (previousState is SpaceManagementLoaded) { + emit(previousState); + } + } + } + + Future> saveSpacesHierarchically( + List spaces, String communityUuid) async { + final orderedSpaces = flattenHierarchy(spaces); + + final parentsToDelete = orderedSpaces.where((space) => + space.status == SpaceStatus.deleted && + (space.parent == null || space.parent?.status != SpaceStatus.deleted)); + + for (var parent in parentsToDelete) { + try { + // Ensure parent.uuid is not null before calling the API + if (parent.uuid != null) { + await _api.deleteSpace(communityUuid, parent.uuid!); + } + } catch (e) { + print( + 'Error deleting space ${parent.name} (UUID: ${parent.uuid}, Community UUID: $communityUuid): $e'); + rethrow; // Decide whether to stop execution or continue + } + } + + for (var space in orderedSpaces) { + try { + if (space.uuid != null && space.uuid!.isNotEmpty) { + final response = await _api.updateSpace( + communityId: communityUuid, + spaceId: space.uuid!, + name: space.name, + parentId: space.parent?.uuid, + isPrivate: space.isPrivate, + position: space.position, + icon: space.icon, + direction: space.incomingConnection?.direction, + products: space.selectedProducts); + } else { + // Call create if the space does not have a UUID + final response = await _api.createSpace( + communityId: communityUuid, + name: space.name, + parentId: space.parent?.uuid, + isPrivate: space.isPrivate, + position: space.position, + icon: space.icon, + direction: space.incomingConnection?.direction, + products: space.selectedProducts); + space.uuid = response?.uuid; + } + } catch (e) { + print('Error creating space ${space.name}: $e'); + rethrow; // Stop further execution on failure + } + } + return spaces; + } + + List flattenHierarchy(List spaces) { + final result = {}; + final topLevelSpaces = spaces.where((space) => space.parent == null); + + void visit(SpaceModel space) { + if (!result.contains(space)) { + result.add(space); + for (var child in spaces.where((s) => s.parent == space)) { + visit(child); + } + } + } + + for (var space in topLevelSpaces) { + visit(space); + } + + for (var space in spaces) { + if (!result.contains(space)) { + result.add(space); + } + } + return result.toList(); // Convert back to a list + } +} diff --git a/lib/pages/spaces_management/bloc/space_management_event.dart b/lib/pages/spaces_management/bloc/space_management_event.dart new file mode 100644 index 00000000..114cd87d --- /dev/null +++ b/lib/pages/spaces_management/bloc/space_management_event.dart @@ -0,0 +1,108 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; // Import for Offset + +abstract class SpaceManagementEvent extends Equatable { + const SpaceManagementEvent(); + + @override + List get props => []; +} + +class LoadCommunityAndSpacesEvent extends SpaceManagementEvent {} + +class DeleteCommunityEvent extends SpaceManagementEvent { + final String communityUuid; + + const DeleteCommunityEvent({ + required this.communityUuid, + }); + + @override + List get props => [communityUuid]; +} + +class CreateSpaceEvent extends SpaceManagementEvent { + final String name; + final String icon; + final Offset position; + final int? parentIndex; + final String? direction; + + const CreateSpaceEvent({ + required this.name, + required this.icon, + required this.position, + this.parentIndex, + this.direction, + }); + + @override + List get props => [ + name, + icon, + position, + parentIndex ?? -1, // Use a fallback value if nullable + direction ?? '', // Use a fallback value if nullable + ]; +} + +class SaveSpacesEvent extends SpaceManagementEvent { + final List spaces; + final String communityUuid; + + const SaveSpacesEvent({ + required this.spaces, + required this.communityUuid, + }); + + @override + List get props => [spaces, communityUuid]; +} + +class UpdateSpacePositionEvent extends SpaceManagementEvent { + final int index; + final Offset newPosition; + + const UpdateSpacePositionEvent(this.index, this.newPosition); + + @override + List get props => [index, newPosition]; +} + +class CreateCommunityEvent extends SpaceManagementEvent { + final String name; + final String description; + + const CreateCommunityEvent({ + required this.name, + required this.description, + }); + + @override + List get props => [name, description]; +} + +class FetchProductsEvent extends SpaceManagementEvent {} + +class LoadSpaceHierarchyEvent extends SpaceManagementEvent { + final String communityId; + + const LoadSpaceHierarchyEvent({required this.communityId}); + + @override + List get props => [communityId]; +} + +class UpdateCommunityEvent extends SpaceManagementEvent { + final String communityUuid; + final String name; + + const UpdateCommunityEvent({ + required this.communityUuid, + required this.name, + }); + + @override + List get props => [communityUuid, name]; +} diff --git a/lib/pages/spaces_management/bloc/space_management_state.dart b/lib/pages/spaces_management/bloc/space_management_state.dart new file mode 100644 index 00000000..0264d4e8 --- /dev/null +++ b/lib/pages/spaces_management/bloc/space_management_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; + +abstract class SpaceManagementState extends Equatable { + const SpaceManagementState(); + + @override + List get props => []; +} + +class SpaceManagementInitial extends SpaceManagementState {} + +class SpaceManagementLoading extends SpaceManagementState {} + +class SpaceManagementLoaded extends SpaceManagementState { + final List communities; + final List products; + CommunityModel? selectedCommunity; // Include products in the state + + SpaceManagementLoaded( + {required this.communities, required this.products, this.selectedCommunity}); +} + +class SpaceCreationSuccess extends SpaceManagementState { + final List spaces; + + const SpaceCreationSuccess({required this.spaces}); + + @override + List get props => [spaces]; +} + +class SpaceManagementError extends SpaceManagementState { + final String errorMessage; + + const SpaceManagementError(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/spaces_management/model/community_model.dart b/lib/pages/spaces_management/model/community_model.dart new file mode 100644 index 00000000..b61b780b --- /dev/null +++ b/lib/pages/spaces_management/model/community_model.dart @@ -0,0 +1,48 @@ +import 'package:syncrow_web/pages/auth/model/region_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; + +class CommunityModel { + final String uuid; + final DateTime createdAt; + final DateTime updatedAt; + String name; + final String description; + final RegionModel? region; + List spaces; + + CommunityModel({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.name, + required this.description, + required this.spaces, + this.region, + }); + + factory CommunityModel.fromJson(Map json) { + return CommunityModel( + uuid: json['uuid'], + createdAt: DateTime.parse(json['createdAt']), + updatedAt: DateTime.parse(json['updatedAt']), + name: json['name'], + description: json['description'], + region: json['region'] != null ? RegionModel.fromJson(json['region']) : null, + spaces: json['spaces'] != null + ? (json['spaces'] as List).map((space) => SpaceModel.fromJson(space)).toList() + : [], + ); + } + + Map toMap() { + return { + 'uuid': uuid, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'name': name, + 'description': description, + 'region': region?.toJson(), + 'spaces': spaces.map((space) => space.toMap()).toList(), // Convert spaces to Map + }; + } +} diff --git a/lib/pages/spaces_management/model/connection_model.dart b/lib/pages/spaces_management/model/connection_model.dart new file mode 100644 index 00000000..650781d6 --- /dev/null +++ b/lib/pages/spaces_management/model/connection_model.dart @@ -0,0 +1,25 @@ +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; + +class Connection { + final SpaceModel startSpace; + final SpaceModel endSpace; + final String direction; + + Connection({required this.startSpace, required this.endSpace, required this.direction}); + + Map toMap() { + return { + 'startUuid': startSpace.uuid ?? 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces + 'endUuid': endSpace.uuid ?? 'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces + 'direction': direction, + }; + } + + static Connection fromMap(Map map, Map spaces) { + return Connection( + startSpace: spaces[map['startUuid']]!, + endSpace: spaces[map['endUuid']]!, + direction: map['direction'], + ); + } +} diff --git a/lib/pages/spaces_management/model/product_model.dart b/lib/pages/spaces_management/model/product_model.dart new file mode 100644 index 00000000..5a0e92e1 --- /dev/null +++ b/lib/pages/spaces_management/model/product_model.dart @@ -0,0 +1,70 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ProductModel { + final String uuid; + final String catName; + String? name; + final String prodId; + final String prodType; + String? icon; + + ProductModel({ + required this.uuid, + required this.catName, + required this.prodId, + required this.prodType, + required this.name, + this.icon, + }); + + // Factory method to create a Product from JSON + factory ProductModel.fromMap(Map json) { + String icon = _mapIconToProduct(json['prodType']); + return ProductModel( + uuid: json['uuid'], + catName: json['catName'], + prodId: json['prodId'], + prodType: json['prodType'], + name: json['name'] ?? '', + icon: _mapIconToProduct(json['prodType'])); + } + + // Method to convert a Product to JSON + Map toMap() { + return { + 'uuid': uuid, + 'catName': catName, + 'prodId': prodId, + 'prodType': prodType, + }; + } + + static String _mapIconToProduct(String prodType) { + const iconMapping = { + '1G': Assets.Gang1SwitchIcon, + '1GT': Assets.oneTouchSwitch, + '2G': Assets.Gang2SwitchIcon, + '2GT': Assets.twoTouchSwitch, + '3G': Assets.Gang3SwitchIcon, + '3GT': Assets.threeTouchSwitch, + 'CUR': Assets.curtain, + 'GD': Assets.garageDoor, + 'GW': Assets.SmartGatewayIcon, + 'DL': Assets.DoorLockIcon, + 'WL': Assets.waterLeakSensor, + 'WH': Assets.waterHeater, + 'AC': Assets.ac, + 'CPS': Assets.presenceSensor, + 'PC': Assets.powerClamp, + 'WPS': Assets.presenceSensor, + 'DS': Assets.doorSensor + }; + + return iconMapping[prodType] ?? Assets.presenceSensor; + } + + @override + String toString() { + return 'ProductModel(uuid: $uuid, catName: $catName, prodId: $prodId, prodType: $prodType, name: $name, icon: $icon)'; + } +} diff --git a/lib/pages/spaces_management/model/selected_product_model.dart b/lib/pages/spaces_management/model/selected_product_model.dart new file mode 100644 index 00000000..9a06698f --- /dev/null +++ b/lib/pages/spaces_management/model/selected_product_model.dart @@ -0,0 +1,18 @@ +class SelectedProduct { + final String productId; + int count; + + SelectedProduct({required this.productId, required this.count}); + + Map toJson() { + return { + 'productId': productId, + 'count': count, + }; + } + + @override + String toString() { + return 'SelectedProduct(productId: $productId, count: $count)'; + } +} diff --git a/lib/pages/spaces_management/model/space_data_model.dart b/lib/pages/spaces_management/model/space_data_model.dart new file mode 100644 index 00000000..74465dad --- /dev/null +++ b/lib/pages/spaces_management/model/space_data_model.dart @@ -0,0 +1,15 @@ +import 'dart:ui'; + +class SpaceData { + final String name; + final String icon; + Offset position; + bool isHovered; + + SpaceData({ + required this.name, + required this.icon, + required this.position, + this.isHovered = false, + }); +} diff --git a/lib/pages/spaces_management/model/space_model.dart b/lib/pages/spaces_management/model/space_model.dart new file mode 100644 index 00000000..6efe19ec --- /dev/null +++ b/lib/pages/spaces_management/model/space_model.dart @@ -0,0 +1,116 @@ +import 'dart:ui'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:uuid/uuid.dart'; + +enum SpaceStatus { newSpace, modified, unchanged, deleted } + +class SpaceModel { + String? uuid; + String? icon; + final String? spaceTuyaUuid; + String name; + final bool isPrivate; + final String? invitationCode; + SpaceModel? parent; + final CommunityModel? community; + List children; + Offset position; + bool isHovered; + SpaceStatus status; + List selectedProducts; + String internalId; + + List outgoingConnections = []; // Connections from this space + Connection? incomingConnection; // Connections to this space + + SpaceModel({ + this.uuid, + String? internalId, + this.spaceTuyaUuid, + required this.icon, + required this.name, + required this.isPrivate, + this.invitationCode, + this.parent, + this.community, + required this.children, + required this.position, + this.isHovered = false, + this.incomingConnection, + this.status = SpaceStatus.unchanged, + this.selectedProducts = const [], + }) : internalId = internalId ?? const Uuid().v4(); + + factory SpaceModel.fromJson(Map json, {String? parentInternalId}) { + final String internalId = json['internalId'] ?? const Uuid().v4(); + + final List children = json['children'] != null + ? (json['children'] as List).map((childJson) { + return SpaceModel.fromJson( + childJson, + parentInternalId: internalId, + ); + }).toList() + : []; + + return SpaceModel( + internalId: internalId, + uuid: json['uuid'] ?? '', + spaceTuyaUuid: json['spaceTuyaUuid'], + name: json['spaceName'], + isPrivate: json['isPrivate'] ?? false, + invitationCode: json['invitationCode'], + parent: parentInternalId != null + ? SpaceModel( + internalId: parentInternalId, + uuid: json['parent']?['uuid'], + spaceTuyaUuid: json['parent']?['spaceTuyaUuid'], + name: json['parent']?['spaceName'] ?? '', + isPrivate: json['parent']?['isPrivate'] ?? false, + invitationCode: json['parent']?['invitationCode'], + children: [], + position: Offset(json['parent']?['x'] ?? 0, json['parent']?['y'] ?? 0), + icon: json['parent']?['icon'] ?? Assets.location, + ) + : null, + community: json['community'] != null ? CommunityModel.fromJson(json['community']) : null, + children: children, + icon: json['icon'] ?? Assets.location, + position: Offset(json['x'] ?? 0, json['y'] ?? 0), + isHovered: false, + selectedProducts: json['spaceProducts'] != null + ? (json['spaceProducts'] as List).map((product) { + return SelectedProduct( + productId: product['product']['uuid'], + count: product['productCount'], + ); + }).toList() + : [], + ); + } + + Map toMap() { + return { + 'uuid': uuid ?? '', + 'spaceTuyaUuid': spaceTuyaUuid, + 'name': name, + 'isPrivate': isPrivate, + 'invitationCode': invitationCode, + 'parent': parent?.uuid, + 'community': community?.toMap(), + 'children': children.map((child) => child.toMap()).toList(), + 'icon': icon, + 'position': {'dx': position.dx, 'dy': position.dy}, + 'isHovered': isHovered, + 'outgoingConnections': outgoingConnections.map((c) => c.toMap()).toList(), + 'incomingConnection': incomingConnection?.toMap(), + }; + } + + void addOutgoingConnection(Connection connection) { + outgoingConnections.add(connection); + } +} diff --git a/lib/pages/spaces_management/model/space_response_model.dart b/lib/pages/spaces_management/model/space_response_model.dart new file mode 100644 index 00000000..34df3d30 --- /dev/null +++ b/lib/pages/spaces_management/model/space_response_model.dart @@ -0,0 +1,39 @@ + +import 'space_model.dart'; + +class SpacesResponse { + final List data; + final String message; + final int page; + final int size; + final int totalItem; + final int totalPage; + final bool hasNext; + final bool hasPrevious; + + SpacesResponse({ + required this.data, + required this.message, + required this.page, + required this.size, + required this.totalItem, + required this.totalPage, + required this.hasNext, + required this.hasPrevious, + }); + + factory SpacesResponse.fromJson(Map json) { + return SpacesResponse( + data: (json['data'] as List) + .map((jsonItem) => SpaceModel.fromJson(jsonItem)) + .toList(), + message: json['message'], + page: json['page'], + size: json['size'], + totalItem: json['totalItem'], + totalPage: json['totalPage'], + hasNext: json['hasNext'], + hasPrevious: json['hasPrevious'], + ); + } +} diff --git a/lib/pages/spaces_management/view/spaces_management_page.dart b/lib/pages/spaces_management/view/spaces_management_page.dart new file mode 100644 index 00000000..54027066 --- /dev/null +++ b/lib/pages/spaces_management/view/spaces_management_page.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_state.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/loaded_space_widget.dart'; +import 'package:syncrow_web/services/product_api.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class SpaceManagementPage extends StatefulWidget { + const SpaceManagementPage({super.key}); + + @override + SpaceManagementPageState createState() => SpaceManagementPageState(); +} + +class SpaceManagementPageState extends State { + CommunityModel? selectedCommunity; + SpaceModel? selectedSpace; + final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi(); + final ProductApi _productApi = ProductApi(); + Map> communitySpaces = {}; + List products = []; + bool isProductDataLoaded = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + SpaceManagementBloc(_api, _productApi)..add(LoadCommunityAndSpacesEvent()), + child: WebScaffold( + appBarTitle: Text('Space Management', style: Theme.of(context).textTheme.headlineLarge), + enableMenuSidebar: false, + rightBody: const NavigateHomeGridView(), + scaffoldBody: + BlocBuilder(builder: (context, state) { + if (state is SpaceManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is SpaceManagementLoaded) { + int selectedIndex = state.communities.indexWhere( + (community) => community.uuid == selectedCommunity?.uuid, + ); + if (selectedIndex != -1) { + selectedCommunity = state.communities[selectedIndex]; + } else if (state.selectedCommunity != null) { + selectedCommunity = state.selectedCommunity; + } else { + selectedCommunity = null; + selectedSpace = null; + } + return LoadedSpaceView( + communities: state.communities, + selectedCommunity: selectedCommunity, + selectedSpace: selectedSpace, + products: state.products, + onCommunitySelected: (community) { + setState(() { + selectedCommunity = community; + }); + }, + onSpaceSelected: (space) { + setState(() { + selectedSpace = space; + }); + }, + ); + } else if (state is SpaceManagementError) { + return Center(child: Text('Error: ${state.errorMessage}')); + } + return Container(); + }), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/add_device_type_widget.dart b/lib/pages/spaces_management/widgets/add_device_type_widget.dart new file mode 100644 index 00000000..08c08997 --- /dev/null +++ b/lib/pages/spaces_management/widgets/add_device_type_widget.dart @@ -0,0 +1,218 @@ +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/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/counter_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'; + +class AddDeviceWidget extends StatefulWidget { + final List? products; + final ValueChanged>? onProductsSelected; + final List? initialSelectedProducts; + + const AddDeviceWidget({ + super.key, + this.products, + this.initialSelectedProducts, + this.onProductsSelected, + }); + + @override + _AddDeviceWidgetState createState() => _AddDeviceWidgetState(); +} + +class _AddDeviceWidgetState extends State { + late final ScrollController _scrollController; + late List productCounts; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + productCounts = + widget.initialSelectedProducts != null ? List.from(widget.initialSelectedProducts!) : []; + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + // Adjust the GridView properties based on screen width + final crossAxisCount = size.width > 1200 + ? 8 + : size.width > 800 + ? 5 + : 3; + + return AlertDialog( + title: const Text('Add Devices'), + backgroundColor: ColorsManager.whiteColors, + content: SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + color: ColorsManager.textFieldGreyColor, + child: Column( + children: [ + const SizedBox(height: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Scrollbar( + controller: _scrollController, + thumbVisibility: false, + child: GridView.builder( + shrinkWrap: true, + controller: _scrollController, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: .8, + ), + itemCount: widget.products?.length ?? 0, + itemBuilder: (context, index) { + final product = widget.products![index]; + return _buildDeviceTypeTile(product, size); + }, + ), + ), + ), + ), + ], + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildActionButton('Cancel', ColorsManager.boxColor, ColorsManager.blackColor, () { + Navigator.of(context).pop(); + }), + _buildActionButton('Continue', ColorsManager.secondaryColor, Colors.white, () { + Navigator.of(context).pop(); + if (widget.onProductsSelected != null) { + widget.onProductsSelected!(productCounts); + } + }), + ], + ), + ], + ); + } + + Widget _buildDeviceTypeTile(ProductModel product, Size size) { + final selectedProduct = productCounts.firstWhere( + (p) => p.productId == product.uuid, + orElse: () => SelectedProduct(productId: product.uuid, count: 0), + ); + + return SizedBox( + width: size.width * 0.12, + height: size.height * 0.15, + child: Card( + elevation: 2, + color: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildDeviceIcon(product, size), + const SizedBox(height: 4), + _buildDeviceName(product, size), + const SizedBox(height: 4), + CounterWidget( + initialCount: selectedProduct.count, + onCountChanged: (newCount) { + setState(() { + if (newCount > 0) { + if (!productCounts.contains(selectedProduct)) { + productCounts + .add(SelectedProduct(productId: product.uuid, count: newCount)); + } else { + selectedProduct.count = newCount; + } + } else { + productCounts.removeWhere((p) => p.productId == product.uuid); + } + + if (widget.onProductsSelected != null) { + widget.onProductsSelected!(productCounts); + } + }); + }, + ), + ], + ), + ), + ), + ); + } + + Widget _buildDeviceIcon(ProductModel product, Size size) { + return Container( + height: size.width > 800 ? 50 : 40, + width: size.width > 800 ? 50 : 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.textFieldGreyColor, + border: Border.all( + color: ColorsManager.neutralGray, + width: 2, + ), + ), + child: Center( + child: SvgPicture.asset( + product.icon ?? Assets.sensors, + width: size.width > 800 ? 30 : 20, + height: size.width > 800 ? 30 : 20, + ), + ), + ); + } + + Widget _buildDeviceName(ProductModel product, Size size) { + return SizedBox( + height: size.width > 800 ? 35 : 25, + child: Text( + product.name ?? '', + style: context.textTheme.bodySmall?.copyWith(color: ColorsManager.blackColor), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + } + + Widget _buildActionButton( + String label, + Color backgroundColor, + Color foregroundColor, + VoidCallback onPressed, + ) { + return SizedBox( + width: 120, + child: DefaultButton( + onPressed: onPressed, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + child: Text(label), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/blank_community_widget.dart b/lib/pages/spaces_management/widgets/blank_community_widget.dart new file mode 100644 index 00000000..6d224562 --- /dev/null +++ b/lib/pages/spaces_management/widgets/blank_community_widget.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/create_community_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class BlankCommunityWidget extends StatelessWidget { + const BlankCommunityWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + color: ColorsManager.whiteColors, // Parent container with white background + child: GridView.builder( + padding: const EdgeInsets.only(left: 40.0, top: 20.0), + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, // Each item's width will be 400 or less + mainAxisSpacing: 10, // Spacing between items + crossAxisSpacing: 10, // Spacing between items + childAspectRatio: 2.0, // Aspect ratio for width:height (e.g., 300:150 = 2.0) + ), + itemCount: 1, // Only one item + itemBuilder: (context, index) { + return GestureDetector( + onTap: () => _showCreateCommunityDialog(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, // Center align the content + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 2.0, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 4, + strokeAlign: BorderSide.strokeAlignOutside, + color: ColorsManager.borderColor, + ), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), + ), + + const SizedBox(height: 9), // Space between item and text + Text('Blank', // Text below the item + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: ColorsManager.blackColor, + )), + ], + )); + }, + ), + ), + ); + } + + void _showCreateCommunityDialog(BuildContext parentContext) { + showDialog( + context: parentContext, + builder: (context) => CreateCommunityDialog( + onCreateCommunity: (String communityName, String description) { + parentContext.read().add( + CreateCommunityEvent( + name: communityName, + description: description, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/community_structure_header_widget.dart b/lib/pages/spaces_management/widgets/community_structure_header_widget.dart new file mode 100644 index 00000000..0addbe87 --- /dev/null +++ b/lib/pages/spaces_management/widgets/community_structure_header_widget.dart @@ -0,0 +1,172 @@ +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/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeader extends StatelessWidget { + final String? communityName; + final bool isEditingName; + final bool isSave; + final TextEditingController nameController; + final VoidCallback onSave; + final VoidCallback onDelete; + final VoidCallback onEditName; + final ValueChanged onNameSubmitted; + + const CommunityStructureHeader({ + Key? key, + required this.communityName, + required this.isSave, + required this.isEditingName, + required this.nameController, + required this.onSave, + required this.onDelete, + required this.onEditName, + required this.onNameSubmitted, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenWidth = MediaQuery.of(context).size.width; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.shadowBlackColor, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildCommunityInfo(theme, screenWidth), + ), + const SizedBox(width: 16), + ], + ), + ], + ), + ); + } + + Widget _buildCommunityInfo(ThemeData theme, double screenWidth) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Community Structure', + style: theme.textTheme.headlineLarge?.copyWith(color: ColorsManager.blackColor), + ), + if (communityName != null) + Row( + children: [ + Expanded( + child: Row( + children: [ + if (!isEditingName) + Flexible( + child: Text( + communityName!, + style: + theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.blackColor), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (isEditingName) + SizedBox( + width: screenWidth * 0.3, + child: TextField( + controller: nameController, + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + ), + style: + theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.blackColor), + onSubmitted: onNameSubmitted, + ), + ), + const SizedBox(width: 2), + GestureDetector( + onTap: onEditName, + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ], + ), + ), + if (isSave) ...[ + const SizedBox(width: 8), + _buildActionButtons(theme), + ], + ], + ), + ], + ); + } + + Widget _buildActionButtons(ThemeData theme) { + return Wrap( + alignment: WrapAlignment.end, + spacing: 10, + children: [ + _buildButton( + label: "Save", + icon: const Icon(Icons.save, size: 18, color: ColorsManager.spaceColor), + onPressed: onSave, + theme: theme), + ], + ); + } + + Widget _buildButton( + {required String label, + required Widget icon, + required VoidCallback onPressed, + required ThemeData theme}) { + const double buttonHeight = 30; + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: 80, minHeight: buttonHeight), + child: DefaultButton( + onPressed: onPressed, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: ColorsManager.blackColor, + borderRadius: 8.0, + padding: 2.0, + height: buttonHeight, + elevation: 0, + borderColor: Colors.grey.shade300, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + icon, + const SizedBox(width: 5), + Flexible( + child: Text( + label, + style: theme.textTheme.bodySmall?.copyWith(color: ColorsManager.blackColor), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/community_structure_widget.dart b/lib/pages/spaces_management/widgets/community_structure_widget.dart new file mode 100644 index 00000000..90937f28 --- /dev/null +++ b/lib/pages/spaces_management/widgets/community_structure_widget.dart @@ -0,0 +1,525 @@ +// Flutter imports +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Syncrow project imports +import 'package:syncrow_web/pages/common/buttons/add_space_button.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/blank_community_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/community_structure_header_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/create_space_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/curved_line_painter.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/space_card_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/space_container_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CommunityStructureArea extends StatefulWidget { + final CommunityModel? selectedCommunity; + SpaceModel? selectedSpace; + final List? products; + final ValueChanged? onSpaceSelected; + + final List spaces; + + CommunityStructureArea({ + this.selectedCommunity, + this.selectedSpace, + this.products, + required this.spaces, + this.onSpaceSelected, + }); + + @override + _CommunityStructureAreaState createState() => _CommunityStructureAreaState(); +} + +class _CommunityStructureAreaState extends State { + double canvasWidth = 1000; + double canvasHeight = 1000; + List spaces = []; + List connections = []; + late TextEditingController _nameController; + bool isEditingName = false; + late TransformationController _transformationController; + + @override + void initState() { + super.initState(); + spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; + connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; + _adjustCanvasSizeForSpaces(); + _nameController = TextEditingController( + text: widget.selectedCommunity?.name ?? '', + ); + _transformationController = TransformationController(); + if (widget.selectedSpace != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _moveToSpace(widget.selectedSpace!); + }); + } + } + + @override + void dispose() { + _nameController.dispose(); + _transformationController.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant CommunityStructureArea oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.spaces != widget.spaces) { + setState(() { + spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; + connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; + _adjustCanvasSizeForSpaces(); + }); + } + + if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _moveToSpace(widget.selectedSpace!); + }); + } + } + + @override + Widget build(BuildContext context) { + if (widget.selectedCommunity == null) { + return BlankCommunityWidget(); + } + + Size screenSize = MediaQuery.of(context).size; + return Expanded( + child: GestureDetector( + onTap: () { + _deselectSpace(); + }, + child: Container( + decoration: const BoxDecoration( + border: Border( + left: BorderSide(color: ColorsManager.whiteColors, width: 1.0), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommunityStructureHeader( + communityName: widget.selectedCommunity?.name, + isSave: isSave(spaces), + isEditingName: isEditingName, + nameController: _nameController, + onSave: _saveSpaces, + onDelete: _onDelete, + onEditName: () { + setState(() { + isEditingName = !isEditingName; + if (isEditingName) { + _nameController.text = widget.selectedCommunity?.name ?? ''; + } + }); + }, + onNameSubmitted: (value) { + context.read().add( + UpdateCommunityEvent( + communityUuid: widget.selectedCommunity!.uuid, + name: value, + ), + ); + setState(() { + widget.selectedCommunity?.name = value; + isEditingName = false; + }); + }, + ), + Flexible( + child: Stack( + children: [ + InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.all(500), + minScale: 0.5, + maxScale: 3.0, + constrained: false, + child: Container( + width: canvasWidth, + height: canvasHeight, + child: Stack( + children: [ + for (var connection in connections) + Opacity( + opacity: + _isHighlightedConnection(connection) ? 1.0 : 0.3, // Adjust opacity + child: CustomPaint(painter: CurvedLinePainter([connection])), + ), + for (var entry in spaces.asMap().entries) + if (entry.value.status != SpaceStatus.deleted) + Positioned( + left: entry.value.position.dx, + top: entry.value.position.dy, + child: SpaceCardWidget( + index: entry.key, + onButtonTap: (int index, Offset newPosition, String direction) { + _showCreateSpaceDialog( + screenSize, + position: spaces[index].position + newPosition, + parentIndex: index, + direction: direction, + ); + }, + position: entry.value.position, + isHovered: entry.value.isHovered, + screenSize: screenSize, + onHoverChanged: _handleHoverChanged, + onPositionChanged: (newPosition) { + _updateNodePosition(entry.value, newPosition); + }, + buildSpaceContainer: (int index) { + final bool isHighlighted = _isHighlightedSpace(spaces[index]); + + return Opacity( + opacity: isHighlighted ? 1.0 : 0.3, + child: SpaceContainerWidget( + index: index, + onDoubleTap: () { + _showEditSpaceDialog(spaces[index]); + }, + onTap: () { + _selectSpace(spaces[index]); + }, + icon: spaces[index].icon ?? '', + name: spaces[index].name, + )); + }, + ), + ), + ], + ), + ), + ), + if (spaces.isEmpty) + Center( + child: AddSpaceButton( + onTap: () { + _showCreateSpaceDialog(screenSize, + canvasHeight: canvasHeight, canvasWidth: canvasWidth); + }, + ), + ), + ], + )), + ], + ), + ), + )); + } + + void _updateNodePosition(SpaceModel node, Offset newPosition) { + setState(() { + node.position = newPosition; + if (node.status != SpaceStatus.newSpace) { + node.status = SpaceStatus.modified; // Mark as modified + } + if (node.position.dx >= canvasWidth - 200) { + canvasWidth += 200; + } + if (node.position.dy >= canvasHeight - 200) { + canvasHeight += 200; + } + if (node.position.dx <= 200) { + double shiftAmount = 200; + canvasWidth += shiftAmount; + for (var n in spaces) { + n.position = Offset(n.position.dx + shiftAmount, n.position.dy); + } + } + if (node.position.dy < 0) { + node.position = Offset(node.position.dx, 0); + } + }); + } + + void _adjustCanvasSizeForSpaces() { + for (var space in spaces) { + if (space.position.dx >= canvasWidth - 200) { + canvasWidth = space.position.dx + 200; + } + + if (space.position.dy >= canvasHeight - 200) { + canvasHeight = space.position.dy + 200; + } + } + } + + void _showCreateSpaceDialog(Size screenSize, + {Offset? position, + int? parentIndex, + String? direction, + double? canvasWidth, + double? canvasHeight}) { + showDialog( + context: context, + builder: (BuildContext context) { + return CreateSpaceDialog( + products: widget.products, + parentSpace: parentIndex != null? spaces[parentIndex] : null, + onCreateSpace: (String name, String icon, List selectedProducts) { + setState(() { + // Set the first space in the center or use passed position + + Offset centerPosition = position ?? _getCenterPosition(screenSize); + SpaceModel newSpace = SpaceModel( + name: name, + icon: icon, + position: centerPosition, + isPrivate: false, + children: [], + status: SpaceStatus.newSpace, + selectedProducts: selectedProducts); + + if (parentIndex != null && direction != null) { + SpaceModel parentSpace = spaces[parentIndex]; + parentSpace.internalId = spaces[parentIndex].internalId; + newSpace.parent = parentSpace; + final newConnection = Connection( + startSpace: parentSpace, + endSpace: newSpace, + direction: direction, + ); + connections.add(newConnection); + newSpace.incomingConnection = newConnection; + parentSpace.addOutgoingConnection(newConnection); + parentSpace.children.add(newSpace); + } + + spaces.add(newSpace); + _updateNodePosition(newSpace, newSpace.position); + }); + }, + ); + }, + ); + } + + void _showEditSpaceDialog(SpaceModel space) { + showDialog( + context: context, + builder: (BuildContext context) { + return CreateSpaceDialog( + products: widget.products, + name: space.name, + icon: space.icon, + isEdit: true, + selectedProducts: space.selectedProducts, + onCreateSpace: (String name, String icon, List selectedProducts) { + setState(() { + // Update the space's properties + space.name = name; + space.icon = icon; + space.selectedProducts = selectedProducts; + + if (space.status != SpaceStatus.newSpace) { + space.status = SpaceStatus.modified; // Mark as modified + } + }); + }, + // Pre-fill the dialog with current space data + key: Key(space.name), // Add a unique key to ensure dialog refresh + ); + }, + ); + } + + void _handleHoverChanged(int index, bool isHovered) { + setState(() { + spaces[index].isHovered = isHovered; + }); + } + + List flattenSpaces(List spaces) { + List result = []; + + void flatten(SpaceModel space) { + if (space.status == SpaceStatus.deleted) return; + + result.add(space); + + for (var child in space.children) { + flatten(child); + } + } + + for (var space in spaces) { + flatten(space); + } + + return result; + } + + List createConnections(List spaces) { + List connections = []; + + void addConnections(SpaceModel parent, String direction) { + if (parent.status == SpaceStatus.deleted) return; + + for (var child in parent.children) { + if (child.status == SpaceStatus.deleted) continue; + + connections.add( + Connection( + startSpace: parent, + endSpace: child, + direction: child.incomingConnection?.direction ?? "down", + ), + ); + + // Recursively process the child's children + addConnections(child, direction); + } + } + + for (var space in spaces) { + addConnections(space, "down"); + } + + return connections; + } + + void _saveSpaces() { + if (widget.selectedCommunity == null) { + debugPrint("No community selected for saving spaces."); + return; + } + + List spacesToSave = spaces.where((space) { + return space.status == SpaceStatus.newSpace || + space.status == SpaceStatus.modified || + space.status == SpaceStatus.deleted; + }).toList(); + + if (spacesToSave.isEmpty) { + debugPrint("No new or modified spaces to save."); + return; + } + + String communityUuid = widget.selectedCommunity!.uuid; + + context.read().add(SaveSpacesEvent( + spaces: spacesToSave, + communityUuid: communityUuid, + )); + } + + void _onDelete() { + if (widget.selectedCommunity != null && + widget.selectedCommunity?.uuid != null && + widget.selectedSpace == null) { + context.read().add(DeleteCommunityEvent( + communityUuid: widget.selectedCommunity!.uuid, + )); + } + if (widget.selectedSpace != null) { + setState(() { + for (var space in spaces) { + if (space.uuid == widget.selectedSpace?.uuid) { + space.status = SpaceStatus.deleted; + _markChildrenAsDeleted(space); + } + } + _removeConnectionsForDeletedSpaces(); + }); + } + } + + void _markChildrenAsDeleted(SpaceModel parent) { + for (var child in parent.children) { + child.status = SpaceStatus.deleted; + _markChildrenAsDeleted(child); + } + } + + void _removeConnectionsForDeletedSpaces() { + connections.removeWhere((connection) { + return connection.startSpace.status == SpaceStatus.deleted || + connection.endSpace.status == SpaceStatus.deleted; + }); + } + + void _moveToSpace(SpaceModel space) { + final double viewportWidth = MediaQuery.of(context).size.width; + final double viewportHeight = MediaQuery.of(context).size.height; + + final double dx = -space.position.dx + (viewportWidth / 2) - 400; + final double dy = -space.position.dy + (viewportHeight / 2) - 300; + + _transformationController.value = Matrix4.identity() + ..translate(dx, dy) + ..scale(1.2); + } + + void _selectSpace(SpaceModel space) { + setState(() { + widget.selectedSpace = space; + }); + + if (widget.onSpaceSelected != null) { + widget.onSpaceSelected!(space); + } + } + + bool _isHighlightedSpace(SpaceModel space) { + if (widget.selectedSpace == null) return true; + if (space == widget.selectedSpace) return true; + if (widget.selectedSpace?.parent?.internalId == space.internalId) return true; + if (widget.selectedSpace?.children != null) { + for (var child in widget.selectedSpace!.children) { + if (child.internalId == space.internalId) { + return true; + } + } + } + return false; + } + + void _deselectSpace() { + if (widget.selectedSpace != null) { + setState(() { + widget.selectedSpace = null; + }); + + if (widget.onSpaceSelected != null) { + widget.onSpaceSelected!(null); // Notify parent that no space is selected + } + } + } + + bool _isHighlightedConnection(Connection connection) { + if (widget.selectedSpace == null) return true; + + return connection.startSpace == widget.selectedSpace || + connection.endSpace == widget.selectedSpace; + } + + Offset _getCenterPosition(Size screenSize) { + return Offset( + screenSize.width / 2 - 260, + screenSize.height / 2 - 200, + ); + } + + bool isSave(List spaces) { + return spaces.isNotEmpty && + spaces.any((space) => + space.status == SpaceStatus.newSpace || + space.status == SpaceStatus.modified || + space.status == SpaceStatus.deleted); + } +} diff --git a/lib/pages/spaces_management/widgets/community_tile.dart b/lib/pages/spaces_management/widgets/community_tile.dart new file mode 100644 index 00000000..5b9f79f2 --- /dev/null +++ b/lib/pages/spaces_management/widgets/community_tile.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final Function(String, bool) onExpansionChanged; + final Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + ); + } +} diff --git a/lib/pages/spaces_management/widgets/counter_widget.dart b/lib/pages/spaces_management/widgets/counter_widget.dart new file mode 100644 index 00000000..66935b12 --- /dev/null +++ b/lib/pages/spaces_management/widgets/counter_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CounterWidget extends StatefulWidget { + final int initialCount; + final ValueChanged onCountChanged; + + const CounterWidget({ + Key? key, + this.initialCount = 0, + required this.onCountChanged, + }) : super(key: key); + + @override + State createState() => _CounterWidgetState(); +} + +class _CounterWidgetState extends State { + late int _counter; + + @override + void initState() { + super.initState(); + _counter = widget.initialCount; + } + + void _incrementCounter() { + setState(() { + _counter++; + widget.onCountChanged(_counter); + }); + } + + void _decrementCounter() { + setState(() { + if (_counter > 0) { + _counter--; + widget.onCountChanged(_counter); + } + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildCounterButton(Icons.remove, _decrementCounter), + const SizedBox(width: 8), + Text( + '$_counter', + style: theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.spaceColor), + ), + const SizedBox(width: 8), + _buildCounterButton(Icons.add, _incrementCounter), + ], + ), + ); + } + + Widget _buildCounterButton(IconData icon, VoidCallback onPressed) { + return GestureDetector( + onTap: onPressed, + child: Icon( + icon, + color: ColorsManager.spaceColor, + size: 18, + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/curved_line_painter.dart b/lib/pages/spaces_management/widgets/curved_line_painter.dart new file mode 100644 index 00000000..263f0edc --- /dev/null +++ b/lib/pages/spaces_management/widgets/curved_line_painter.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CurvedLinePainter extends CustomPainter { + final List connections; + + CurvedLinePainter(this.connections); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = ColorsManager.blackColor + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; + + // Ensure connections exist before painting + if (connections.isEmpty) { + return; // Nothing to paint if there are no connections + } + + for (var connection in connections) { + // Ensure positions are valid before drawing lines + if (connection.endSpace.position == null) { + continue; + } + + Offset start = connection.startSpace.position + + const Offset(75, 60); // Center bottom of start space + Offset end = connection.endSpace.position + + const Offset(75, 0); // Center top of end space + + if (connection.direction == 'down') { + // Curved line for down connections + final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50); + final path = Path() + ..moveTo(start.dx, start.dy) + ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy); + canvas.drawPath(path, paint); + } else if (connection.direction == 'right') { + start = connection.startSpace.position + + const Offset(150, 30); // Right center + end = connection.endSpace.position + const Offset(0, 30); // Left center + + canvas.drawLine(start, end, paint); + } else if (connection.direction == 'left') { + start = + connection.startSpace.position + const Offset(0, 30); // Left center + end = connection.endSpace.position + + const Offset(150, 30); // Right center + + canvas.drawLine(start, end, paint); + } + + final dotPaint = Paint()..color = ColorsManager.blackColor; + canvas.drawCircle(start, 5, dotPaint); // Start dot + canvas.drawCircle(end, 5, dotPaint); // End dot + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart b/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart new file mode 100644 index 00000000..135ada3b --- /dev/null +++ b/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialog extends StatefulWidget { + final Function(String name, String description) onCreateCommunity; + + const CreateCommunityDialog({super.key, required this.onCreateCommunity}); + + @override + CreateCommunityDialogState createState() => CreateCommunityDialogState(); +} + +class CreateCommunityDialogState extends State { + String enteredName = ''; // Store the entered community name + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: Colors.transparent, // Transparent for shadow effect + child: Stack( + children: [ + // Background container with shadow and rounded corners + Container( + width: 490, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Community Name', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + // Input field for the community name + TextField( + onChanged: (value) { + enteredName = value; // Capture entered name + }, + style: const TextStyle( + color: Colors.black, + ), + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + hintStyle: const TextStyle( + fontSize: 14, + color: ColorsManager.grayBorder, + fontWeight: FontWeight.w400, + ), + border: OutlineInputBorder( + borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1), + borderRadius: BorderRadius.circular(10), + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1), + ), + ), + ), + const SizedBox(height: 24), + // Action buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: ColorsManager.boxColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.black), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + if (enteredName.isNotEmpty) { + widget.onCreateCommunity( + enteredName, + "", + ); + Navigator.of(context).pop(); + } + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text('OK'), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/dialogs/create_space_dialog.dart b/lib/pages/spaces_management/widgets/dialogs/create_space_dialog.dart new file mode 100644 index 00000000..eba5096c --- /dev/null +++ b/lib/pages/spaces_management/widgets/dialogs/create_space_dialog.dart @@ -0,0 +1,383 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/add_device_type_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/icon_selection_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/hoverable_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/constants/space_icon_const.dart'; + +class CreateSpaceDialog extends StatefulWidget { + final Function(String, String, List selectedProducts) onCreateSpace; + final List? products; + final String? name; + final String? icon; + final bool isEdit; + final List selectedProducts; + final SpaceModel? parentSpace; + + const CreateSpaceDialog( + {super.key, + this.parentSpace, + required this.onCreateSpace, + this.products, + this.name, + this.icon, + this.isEdit = false, + this.selectedProducts = const []}); + + @override + CreateSpaceDialogState createState() => CreateSpaceDialogState(); +} + +class CreateSpaceDialogState extends State { + String selectedIcon = Assets.location; + String enteredName = ''; + List selectedProducts = []; + late TextEditingController nameController; + bool isOkButtonEnabled = false; + bool isNameFieldInvalid = false; + bool isNameFieldExist = false; + + @override + void initState() { + super.initState(); + selectedIcon = widget.icon ?? Assets.location; + nameController = TextEditingController(text: widget.name ?? ''); + selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : []; + isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty; + } + + @override + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return AlertDialog( + title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'), + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + width: screenWidth * 0.5, // Limit dialog width + child: SingleChildScrollView( + // Scrollable content to prevent overflow + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + width: screenWidth * 0.1, // Adjusted width + height: screenWidth * 0.1, // Adjusted height + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + ), + SvgPicture.asset( + selectedIcon, + width: screenWidth * 0.04, + height: screenWidth * 0.04, + ), + Positioned( + top: 6, + right: 6, + child: InkWell( + onTap: _showIconSelectionDialog, + child: Container( + width: screenWidth * 0.020, + height: screenWidth * 0.020, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: screenWidth * 0.06, + height: screenWidth * 0.06, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + // Ensure the text field expands responsively + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: nameController, + onChanged: (value) { + enteredName = value.trim(); + setState(() { + isNameFieldExist = false; + isOkButtonEnabled = false; + isNameFieldInvalid = value.isEmpty; + + if (!isNameFieldInvalid) { + if (widget.parentSpace?.children + .any((child) => child.name == value) ?? + false) { + isNameFieldExist = true; + } else { + isOkButtonEnabled = true; + } + } + }); + }, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: const TextStyle( + fontSize: 14, + color: ColorsManager.lightGrayColor, + fontWeight: FontWeight.w400, + ), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: isNameFieldInvalid || isNameFieldExist + ? ColorsManager.red + : ColorsManager.boxColor, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + if (isNameFieldInvalid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Space name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameFieldExist) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exist', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + const SizedBox(height: 16), + if (selectedProducts.isNotEmpty) + _buildSelectedProductsButtons(widget.products ?? []) + else + DefaultButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AddDeviceWidget( + products: widget.products, + onProductsSelected: (selectedProductsMap) { + setState(() { + selectedProducts = selectedProductsMap; + }); + }, + ), + ); + }, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: Colors.black, + borderColor: ColorsManager.neutralGray, + borderRadius: 16.0, + padding: 10.0, // Reduced padding for smaller size + child: Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + Assets.addIcon, + width: screenWidth * 0.015, // Adjust icon size + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 3), + Flexible( + child: Text( + 'Add devices / Assign a space model', + overflow: TextOverflow.ellipsis, // Prevent overflow + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + )), + ], + ), + ), + ], + ), + ], + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: () { + if (nameController.text.isEmpty) { + setState(() { + isNameFieldInvalid = true; + }); + return; + } else { + String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? ''); + if (newName.isNotEmpty) { + widget.onCreateSpace(newName, selectedIcon, selectedProducts); + Navigator.of(context).pop(); + } + } + }, + borderRadius: 10, + backgroundColor: + isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ); + } + + void _showIconSelectionDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return IconSelectionDialog( + spaceIconList: spaceIconList, + onIconSelected: (String selectedIcon) { + setState(() { + this.selectedIcon = selectedIcon; + }); + }, + ); + }, + ); + } + + Widget _buildSelectedProductsButtons(List products) { + final screenWidth = MediaQuery.of(context).size.width; + + return Container( + width: screenWidth * 0.6, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: ColorsManager.neutralGray, + width: 2, + ), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; i < selectedProducts.length; i++) ...[ + HoverableButton( + iconPath: _mapIconToProduct(selectedProducts[i].productId, products), + text: 'x${selectedProducts[i].count}', + onTap: () { + setState(() { + selectedProducts.remove(selectedProducts[i]); + }); + // Handle button tap + }, + ), + if (i < selectedProducts.length - 1) + const SizedBox(width: 2), // Add space except after the last button + ], + const SizedBox(width: 2), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => AddDeviceWidget( + products: widget.products, + initialSelectedProducts: selectedProducts, + onProductsSelected: (selectedProductsMap) { + setState(() { + selectedProducts = selectedProductsMap; + }); + }, + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.add, + color: ColorsManager.spaceColor, + size: 24, + ), + ), + ), + ], + ), + ); + } + + String _mapIconToProduct(String uuid, List products) { + // Find the product with the matching UUID + final product = products.firstWhere( + (product) => product.uuid == uuid, + orElse: () => ProductModel( + uuid: '', + catName: '', + prodId: '', + prodType: '', + name: '', + icon: Assets.presenceSensor, + ), + ); + + return product.icon ?? Assets.presenceSensor; + } +} diff --git a/lib/pages/spaces_management/widgets/dialogs/delete_dialogue.dart b/lib/pages/spaces_management/widgets/dialogs/delete_dialogue.dart new file mode 100644 index 00000000..8607f9e0 --- /dev/null +++ b/lib/pages/spaces_management/widgets/dialogs/delete_dialogue.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +void showDeleteConfirmationDialog(BuildContext context, VoidCallback onConfirm, bool isSpace) { + final String title = isSpace ? 'Delete Space' : 'Delete Community'; + final String subtitle = isSpace + ? 'All the data in the space will be lost' + : 'All the data in the community will be lost'; + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), + child: SizedBox( + width: 500, + child: Container( + color: ColorsManager.whiteColors, + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildWarningIcon(), + const SizedBox(height: 20), + _buildDialogTitle(context, title), + const SizedBox(height: 10), + _buildDialogSubtitle(context, subtitle), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); // Close the first dialog + showProcessingPopup(context, isSpace, onConfirm); + }, + style: _dialogButtonStyle(Colors.blue), + child: const Text('Continue', style: TextStyle(color: Colors.white)), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); +} + +void showProcessingPopup(BuildContext context, bool isSpace, VoidCallback onDelete) { + final String title = isSpace ? 'Delete Space' : 'Delete Community'; + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), + child: SizedBox( + width: 500, + child: Container( + color: ColorsManager.whiteColors, + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildWarningIcon(), + const SizedBox(height: 20), + _buildDialogTitle(context, title), + const SizedBox(height: 10), + _buildDialogSubtitle(context, 'Are you sure you want to delete?'), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: onDelete, + style: _dialogButtonStyle(ColorsManager.warningRed), + child: const Text('Delete', style: TextStyle(color: Colors.white)), + ), + CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); +} + +Widget _buildWarningIcon() { + return Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: ColorsManager.warningRed, + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, color: Colors.white, size: 40), + ); +} + +Widget _buildDialogTitle(BuildContext context, String title) { + return Text( + title, + style: Theme.of(context).textTheme.headlineMedium, + ); +} + +Widget _buildDialogSubtitle(BuildContext context, String subtitle) { + return Text( + subtitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: ColorsManager.grayColor), + ); +} + +ButtonStyle _dialogButtonStyle(Color color) { + return ElevatedButton.styleFrom( + backgroundColor: color, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), + fixedSize: const Size(140, 40), + ); +} diff --git a/lib/pages/spaces_management/widgets/dialogs/icon_selection_dialog.dart b/lib/pages/spaces_management/widgets/dialogs/icon_selection_dialog.dart new file mode 100644 index 00000000..5251ba32 --- /dev/null +++ b/lib/pages/spaces_management/widgets/dialogs/icon_selection_dialog.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class IconSelectionDialog extends StatelessWidget { + final List spaceIconList; + final Function(String selectedIcon) onIconSelected; + + const IconSelectionDialog({ + Key? key, + required this.spaceIconList, + required this.onIconSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + + return Dialog( + elevation: 0, + backgroundColor: ColorsManager.transparentColor, + child: Container( + width: screenWidth * 0.44, + height: screenHeight * 0.45, + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), // Shadow color + blurRadius: 20, // Spread of the blur + offset: const Offset(0, 8), // Offset of the shadow + ), + ], + ), + child: AlertDialog( + title: Text('Space Icon',style: Theme.of(context).textTheme.headlineMedium), + backgroundColor: ColorsManager.whiteColors, + content: Container( + width: screenWidth * 0.4, + height: screenHeight * 0.45, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(12), + ), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 8, + mainAxisSpacing: 16, + ), + itemCount: spaceIconList.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + onIconSelected(spaceIconList[index]); + Navigator.of(context).pop(); + }, + child: SvgPicture.asset( + spaceIconList[index], + width: screenWidth * 0.03, + height: screenWidth * 0.03, + ), + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/gradient_canvas_border_widget.dart b/lib/pages/spaces_management/widgets/gradient_canvas_border_widget.dart new file mode 100644 index 00000000..e1d4e11b --- /dev/null +++ b/lib/pages/spaces_management/widgets/gradient_canvas_border_widget.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class GradientCanvasBorderWidget extends StatelessWidget { + final double top; + final double bottom; + final double left; + final double width; + + const GradientCanvasBorderWidget({ + super.key, + this.top = 0, + this.bottom = 0, + this.left = 300, + this.width = 8, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + top: top, + bottom: bottom, + left: left, + width: width, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + ColorsManager.semiTransparentBlackColor.withOpacity(0.1), + ColorsManager.transparentColor, + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/hoverable_button.dart b/lib/pages/spaces_management/widgets/hoverable_button.dart new file mode 100644 index 00000000..49a863b6 --- /dev/null +++ b/lib/pages/spaces_management/widgets/hoverable_button.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class HoverableButton extends StatefulWidget { + final String iconPath; + final String text; + final VoidCallback onTap; + + const HoverableButton({ + Key? key, + required this.iconPath, + required this.text, + required this.onTap, + }) : super(key: key); + + @override + State createState() => _HoverableButtonState(); +} + +class _HoverableButtonState extends State { + bool isHovered = false; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenWidth = MediaQuery.of(context).size.width; + + return GestureDetector( + onTap: widget.onTap, + child: MouseRegion( + onEnter: (_) => _updateHoverState(true), + onExit: (_) => _updateHoverState(false), + child: SizedBox( + width: screenWidth * .07, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 8), + decoration: BoxDecoration( + color: isHovered ? ColorsManager.warningRed : ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + if (isHovered) + BoxShadow( + color: ColorsManager.warningRed.withOpacity(0.4), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildIcon(), + if (!isHovered) const SizedBox(width: 8), + if (!isHovered) _buildText(theme), + ], + ), + )), + )), + ); + } + + Widget _buildIcon() { + return isHovered + ? const Icon( + Icons.close, + color: ColorsManager.whiteColors, + size: 24, + ) + : SvgPicture.asset( + widget.iconPath, + width: 24, + height: 24, + ); + } + + Widget _buildText(ThemeData theme) { + return Text( + widget.text, + style: theme.textTheme.bodyLarge?.copyWith( + color: ColorsManager.spaceColor, + fontWeight: FontWeight.w500, + ), + ); + } + + void _updateHoverState(bool hover) { + setState(() => isHovered = hover); + } +} diff --git a/lib/pages/spaces_management/widgets/loaded_space_widget.dart b/lib/pages/spaces_management/widgets/loaded_space_widget.dart new file mode 100644 index 00000000..aa48d323 --- /dev/null +++ b/lib/pages/spaces_management/widgets/loaded_space_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/community_structure_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/gradient_canvas_border_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/sidebar_widget.dart'; + +class LoadedSpaceView extends StatefulWidget { + final List communities; + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + final ValueChanged? onCommunitySelected; + final ValueChanged? onSpaceSelected; + final List? products; + + const LoadedSpaceView({ + super.key, + required this.communities, + this.selectedCommunity, + this.selectedSpace, + required this.onCommunitySelected, + required this.onSpaceSelected, + this.products, + }); + + @override + _LoadedStateViewState createState() => _LoadedStateViewState(); +} + +class _LoadedStateViewState extends State { + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + Row( + children: [ + SidebarWidget( + communities: widget.communities, + onCommunitySelected: widget.onCommunitySelected, + onSpaceSelected: widget.onSpaceSelected, + selectedSpaceUuid: widget.selectedSpace?.uuid, + onSelectedSpaceChanged: (String? spaceUuid) { + setState(() { + final selectedSpace = findSpaceByUuid(spaceUuid, widget.communities); + if (selectedSpace != null) { + widget.onSpaceSelected!(selectedSpace); + } + }); + }), + CommunityStructureArea( + selectedCommunity: widget.selectedCommunity, + selectedSpace: widget.selectedSpace, + spaces: widget.selectedCommunity?.spaces ?? [], + products: widget.products, + onSpaceSelected: (SpaceModel? space) { + setState(() { + widget.onSpaceSelected!(space); + }); + }, + ), + ], + ), + const GradientCanvasBorderWidget(), + ], + ); + } + + SpaceModel? findSpaceByUuid(String? uuid, List communities) { + for (var community in communities) { + for (var space in community.spaces) { + if (space.uuid == uuid) return space; + } + } + return null; + } +} diff --git a/lib/pages/spaces_management/widgets/plus_button_widget.dart b/lib/pages/spaces_management/widgets/plus_button_widget.dart new file mode 100644 index 00000000..b077ac9d --- /dev/null +++ b/lib/pages/spaces_management/widgets/plus_button_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PlusButtonWidget extends StatelessWidget { + final int index; + final String direction; + final Offset offset; + final Function(int index, Offset newPosition, String direction) onButtonTap; + + const PlusButtonWidget({ + super.key, + required this.index, + required this.direction, + required this.offset, + required this.onButtonTap, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + left: offset.dx, + top: offset.dy, + child: GestureDetector( + onTap: () { + Offset newPosition; + switch (direction) { + case 'left': + newPosition = const Offset(-200, 0); + break; + case 'right': + newPosition = const Offset(200, 0); + break; + case 'down': + newPosition = const Offset(0, 150); + break; + default: + newPosition = Offset.zero; + } + onButtonTap(index, newPosition, direction); + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon(Icons.add, color: Colors.white, size: 20), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/sidebar_widget.dart b/lib/pages/spaces_management/widgets/sidebar_widget.dart new file mode 100644 index 00000000..507cd703 --- /dev/null +++ b/lib/pages/spaces_management/widgets/sidebar_widget.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/common/search_bar.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/create_community_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/widgets/space_tile_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SidebarWidget extends StatefulWidget { + final Function(CommunityModel)? onCommunitySelected; + final Function(SpaceModel?)? onSpaceSelected; + final List communities; + final Function(String?)? onSelectedSpaceChanged; // New callback + + final String? selectedSpaceUuid; + + const SidebarWidget({ + super.key, + this.onCommunitySelected, + this.onSpaceSelected, + this.onSelectedSpaceChanged, + required this.communities, + this.selectedSpaceUuid, + }); + + @override + _SidebarWidgetState createState() => _SidebarWidgetState(); +} + +class _SidebarWidgetState extends State { + String _searchQuery = ''; // Track search query + String? _selectedSpaceUuid; + String? _selectedId; + + @override + void initState() { + super.initState(); + _selectedId = widget.selectedSpaceUuid; // Initialize with the passed selected space UUID + } + + @override + void didUpdateWidget(covariant SidebarWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) { + setState(() { + _selectedId = widget.selectedSpaceUuid; + }); + } + } + + void _showCreateCommunityDialog(BuildContext parentContext) { + showDialog( + context: parentContext, + builder: (context) => CreateCommunityDialog( + onCreateCommunity: (String communityName, String description) { + parentContext.read().add( + CreateCommunityEvent( + name: communityName, + description: description, + ), + ); + }, + ), + ); + } + + // Function to filter communities based on the search query + List _filterCommunities() { + if (_searchQuery.isEmpty) { + // Reset the selected community and space UUIDs if there's no query + _selectedSpaceUuid = null; + return widget.communities; + } + + // Filter communities and expand only those that match the query + return widget.communities.where((community) { + final containsQueryInCommunity = + community.name.toLowerCase().contains(_searchQuery.toLowerCase()); + final containsQueryInSpaces = + community.spaces.any((space) => _containsQuery(space, _searchQuery.toLowerCase())); + + return containsQueryInCommunity || containsQueryInSpaces; + }).toList(); + } + + // Helper function to determine if any space or its children match the search query + bool _containsQuery(SpaceModel space, String query) { + final matchesSpace = space.name.toLowerCase().contains(query); + final matchesChildren = + space.children.any((child) => _containsQuery(child, query)); // Recursive check for children + + // If the space or any of its children match the query, expand this space + if (matchesSpace || matchesChildren) { + _selectedSpaceUuid = space.uuid; + } + + return matchesSpace || matchesChildren; + } + + bool _isSpaceOrChildSelected(SpaceModel space) { + // Return true if the current space or any of its child spaces is selected + if (_selectedSpaceUuid == space.uuid) { + return true; + } + + // Recursively check if any child spaces match the query + for (var child in space.children) { + if (_isSpaceOrChildSelected(child)) { + return true; + } + } + + return false; + } + + @override + Widget build(BuildContext context) { + final filteredCommunities = _filterCommunities(); + + return Container( + width: 300, + decoration: subSectionContainerDecoration, + child: Column( + mainAxisSize: MainAxisSize.min, // Ensures the Column only takes necessary height + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Communities title with the add button + Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Communities', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.black, + )), + GestureDetector( + onTap: () => _showCreateCommunityDialog(context), + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + shape: BoxShape.circle, + ), + child: Center( + child: SvgPicture.asset( + Assets.roundedAddIcon, + width: 24, + height: 24, + ), + ), + ), + ), + ], + ), + ), + // Search bar + CustomSearchBar( + onSearchChanged: (query) { + setState(() { + _searchQuery = query; + }); + }, + ), + const SizedBox(height: 16), + // Community list + Expanded( + child: ListView( + children: filteredCommunities.map((community) { + return _buildCommunityTile(community); + }).toList(), + ), + ), + ], + ), + ); + } + + Widget _buildCommunityTile(CommunityModel community) { + bool hasChildren = community.spaces.isNotEmpty; + + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: _selectedId == community.uuid, + isExpanded: false, + onItemSelected: () { + setState(() { + _selectedId = community.uuid; + _selectedSpaceUuid = null; // Update the selected community + }); + + if (widget.onCommunitySelected != null) { + widget.onCommunitySelected!(community); + widget.onSpaceSelected!(null); // Pass the entire community + } + }, + onExpansionChanged: (String title, bool expanded) { + _handleExpansionChange(community.uuid, expanded); + }, + children: hasChildren + ? community.spaces.map((space) => _buildSpaceTile(space, community)).toList() + : null, // Render spaces within the community + ); + } + + Widget _buildSpaceTile(SpaceModel space, CommunityModel community) { + bool isExpandedSpace = _isSpaceOrChildSelected(space); +// Check if space should be expanded + return SpaceTile( + title: space.name, + key: ValueKey(space.uuid), + isSelected: _selectedId == space.uuid, + initiallyExpanded: isExpandedSpace, + onExpansionChanged: (bool expanded) { + _handleExpansionChange(space.uuid ?? '', expanded); + }, + onItemSelected: () { + setState(() { + _selectedId = space.uuid; + _selectedSpaceUuid = space.uuid; + }); + + if (widget.onSpaceSelected != null) { + widget.onCommunitySelected!(community); + widget.onSpaceSelected!(space); + } + + if (widget.onSelectedSpaceChanged != null) { + widget.onSelectedSpaceChanged!(space.uuid); + } + }, + children: space.children.isNotEmpty + ? space.children.map((childSpace) => _buildSpaceTile(childSpace, community)).toList() + : [], // Recursively render child spaces if available + ); + } + + void _handleExpansionChange(String uuid, bool expanded) {} +} diff --git a/lib/pages/spaces_management/widgets/space_card_widget.dart b/lib/pages/spaces_management/widgets/space_card_widget.dart new file mode 100644 index 00000000..0a78da52 --- /dev/null +++ b/lib/pages/spaces_management/widgets/space_card_widget.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'plus_button_widget.dart'; // Make sure to import your PlusButtonWidget + +class SpaceCardWidget extends StatelessWidget { + final int index; + final Size screenSize; + final Offset position; + final bool isHovered; + final Function(int index, bool isHovered) onHoverChanged; + final Function(int index, Offset newPosition, String direction) onButtonTap; + final Widget Function(int index) buildSpaceContainer; + final ValueChanged onPositionChanged; + + const SpaceCardWidget({ + super.key, + required this.index, + required this.onPositionChanged, + required this.screenSize, + required this.position, + required this.isHovered, + required this.onHoverChanged, + required this.onButtonTap, + required this.buildSpaceContainer, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onPanUpdate: (details) { + // Call the provided callback to update the position + final newPosition = position + details.delta; + onPositionChanged(newPosition); + }, + child: MouseRegion( + onEnter: (_) { + // Call the provided callback to handle hover state + onHoverChanged(index, true); + }, + onExit: (_) { + // Call the provided callback to handle hover state + onHoverChanged(index, false); + }, + child: Stack( + clipBehavior: Clip + .none, // Allow hovering elements to be displayed outside the boundary + children: [ + buildSpaceContainer(index), // Build the space container + if (isHovered) ...[ + PlusButtonWidget( + index: index, + direction: 'left', + offset: const Offset(-21, 20), + onButtonTap: onButtonTap, + ), + PlusButtonWidget( + index: index, + direction: 'right', + offset: const Offset(140, 20), + onButtonTap: onButtonTap, + ), + PlusButtonWidget( + index: index, + direction: 'down', + offset: const Offset(63, 50), + onButtonTap: onButtonTap, + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/widgets/space_container_widget.dart b/lib/pages/spaces_management/widgets/space_container_widget.dart new file mode 100644 index 00000000..78af5f10 --- /dev/null +++ b/lib/pages/spaces_management/widgets/space_container_widget.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceContainerWidget extends StatelessWidget { + final int index; + final String icon; + final String name; + final VoidCallback? onDoubleTap; + final VoidCallback? onTap; + + const SpaceContainerWidget({ + super.key, + required this.index, + required this.icon, + required this.name, + this.onTap, + this.onDoubleTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GestureDetector( + onDoubleTap: onDoubleTap, + onTap: onTap, + child: Container( + width: 150, + height: 60, + decoration: _containerDecoration(), + child: Row( + children: [ + _buildIconContainer(), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, // Handle long names gracefully + ), + ), + ], + ), + ), + ); + } + + /// Builds the icon container with the SVG asset. + Widget _buildIconContainer() { + return Container( + width: 40, + height: double.infinity, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + color: ColorsManager.whiteColors, + width: 24, + height: 24, + ), + ), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), // Shadow position + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/widgets/space_tile_widget.dart b/lib/pages/spaces_management/widgets/space_tile_widget.dart new file mode 100644 index 00000000..80753f9d --- /dev/null +++ b/lib/pages/spaces_management/widgets/space_tile_widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + _SpaceTileState createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ); + } +} diff --git a/lib/pages/spaces_management/widgets/space_widget.dart b/lib/pages/spaces_management/widgets/space_widget.dart new file mode 100644 index 00000000..e4ce27cc --- /dev/null +++ b/lib/pages/spaces_management/widgets/space_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class SpaceWidget extends StatelessWidget { + final String name; + final Offset position; + final VoidCallback onTap; + + const SpaceWidget({ + super.key, + required this.name, + required this.position, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + left: position.dx, + top: position.dy, + child: GestureDetector( + onTap: onTap, + child: + Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + const Icon(Icons.location_on, color: Colors.blue), + const SizedBox(width: 8), + Text(name, style: const TextStyle(fontSize: 16)), + ], + ), + ), + + ), + ); + } +} diff --git a/lib/services/product_api.dart b/lib/services/product_api.dart new file mode 100644 index 00000000..f33a4135 --- /dev/null +++ b/lib/services/product_api.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class ProductApi { + Future> fetchProducts() async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.listProducts, + expectedResponseModel: (json) { + List jsonData = json['data']; + + List productList = jsonData.map((jsonItem) { + return ProductModel.fromMap(jsonItem); + }).toList(); + return productList; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching products: $e'); + return []; + } + } +} diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart new file mode 100644 index 00000000..01248262 --- /dev/null +++ b/lib/services/space_mana_api.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_response_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class CommunitySpaceManagementApi { + // Community Management APIs + Future> fetchCommunities() async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.getCommunityList, + expectedResponseModel: (json) { + // Access the 'data' key from the response + List jsonData = json['data']; + + // Check if jsonData is actually a List + List communityList = jsonData.map((jsonItem) { + return CommunityModel.fromJson(jsonItem); + }).toList(); + return communityList; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching communities: $e'); + return []; + } + } + + Future getCommunityById(String communityId) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), + expectedResponseModel: (json) { + return CommunityModel.fromJson(json['data']); + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching community by ID: $e'); + return null; + } + } + + Future createCommunity(String name, String description) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.createCommunity, + body: { + 'name': name, + 'description': description, + }, + expectedResponseModel: (json) { + return CommunityModel.fromJson(json['data']); + }, + ); + return response; + } catch (e) { + debugPrint('Error creating community: $e'); + return null; + } + } + + Future updateCommunity(String communityId, String name) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.updateCommunity.replaceAll('{communityId}', communityId), + body: { + 'name': name, + }, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error updating community: $e'); + return false; + } + } + + Future deleteCommunity(String communityId) async { + try { + final response = await HTTPService().delete( + path: ApiEndpoints.deleteCommunity.replaceAll('{communityId}', communityId), + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error deleting community: $e'); + return false; + } + } + + Future fetchSpaces(String communityId) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.listSpaces.replaceAll('{communityId}', communityId), + expectedResponseModel: (json) { + return SpacesResponse.fromJson(json); + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching spaces: $e'); + return SpacesResponse( + data: [], + message: 'Error fetching spaces', + page: 1, + size: 10, + totalItem: 0, + totalPage: 0, + hasNext: false, + hasPrevious: false, + ); + } + } + + Future getSpace(String communityId, String spaceId) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.getSpace + .replaceAll('{communityId}', communityId) + .replaceAll('{spaceId}', spaceId), + expectedResponseModel: (json) { + return SpaceModel.fromJson(json); + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching space: $e'); + return null; // Assuming an empty SpaceModel constructor + } + } + + Future createSpace({ + required String communityId, + required String name, + String? parentId, + String? direction, + bool isPrivate = false, + required Offset position, + String? icon, + required List products, + }) async { + try { + final body = { + 'spaceName': name, + 'isPrivate': isPrivate, + 'x': position.dx, + 'y': position.dy, + 'direction': direction, + 'icon': icon, + 'products': products.map((product) => product.toJson()).toList(), + }; + if (parentId != null) { + body['parentUuid'] = parentId; + } + final response = await HTTPService().post( + path: ApiEndpoints.createSpace.replaceAll('{communityId}', communityId), + body: body, + expectedResponseModel: (json) { + return SpaceModel.fromJson(json['data']); + }, + ); + return response; + } catch (e) { + debugPrint('Error creating space: $e'); + return null; + } + } + + Future updateSpace({ + required String communityId, + required spaceId, + required String name, + String? parentId, + String? icon, + String? direction, + bool isPrivate = false, + required Offset position, + required List products, + }) async { + try { + final body = { + 'spaceName': name, + 'isPrivate': isPrivate, + 'x': position.dx, + 'y': position.dy, + 'direction': direction, + 'icon': icon, + 'products': products.map((product) => product.toJson()).toList(), + }; + if (parentId != null) { + body['parentUuid'] = parentId; + } + + final response = await HTTPService().put( + path: ApiEndpoints.updateSpace + .replaceAll('{communityId}', communityId) + .replaceAll('{spaceId}', spaceId), + body: body, + expectedResponseModel: (json) { + return SpaceModel.fromJson(json['data']); + }, + ); + return response; + } catch (e) { + debugPrint('Error creating space: $e'); + return null; + } + } + + Future deleteSpace(String communityId, String spaceId) async { + try { + final response = await HTTPService().delete( + path: ApiEndpoints.deleteSpace + .replaceAll('{communityId}', communityId) + .replaceAll('{spaceId}', spaceId), + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error deleting space: $e'); + return false; + } + } + + Future> getSpaceHierarchy(String communityId) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.getSpaceHierarchy.replaceAll('{communityId}', communityId), + expectedResponseModel: (json) { + final spaceModels = + (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + + return spaceModels; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching space hierarchy: $e'); + return []; + } + } +} diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index a730341b..20a89e21 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -3,6 +3,7 @@ import 'package:syncrow_web/pages/access_management/view/access_management.dart' import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; +import 'package:syncrow_web/pages/spaces_management/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; @@ -29,6 +30,9 @@ class AppRoutes { path: RoutesConst.deviceManagementPage, builder: (context, state) => const DeviceManagementPage(), ), + GoRoute( + path: RoutesConst.spacesManagementPage, + builder: (context, state) => SpaceManagementPage()), ]; } } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 140ed370..95d0f214 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -9,14 +9,16 @@ abstract class ColorsManager { static const Color whiteColors = Colors.white; static const Color secondaryColor = Color(0xFF023DFE); static const Color onSecondaryColor = Color(0xFF023DFE); + static Color shadowBlackColor = Colors.black.withOpacity(0.2); - static Color dialogBlueTitle = Color(0xFF023DFE).withOpacity(0.6); + static Color dialogBlueTitle = const Color(0xFF023DFE).withOpacity(0.6); static const Color primaryTextColor = Colors.black; static const Color greyColor = Color(0xFFd5d5d5); static const Color lightGreyColor = Color(0xFF999999); static const Color backgroundColor = Color(0xFFececec); + static const Color textFieldGreyColor = Color(0xFFF4F4F4); static const Color dozeColor = Color(0xFFFEC258); static const Color relaxColor = Color(0xFFFBD288); static const Color readingColor = Color(0xFFF7D69C); @@ -30,6 +32,8 @@ abstract class ColorsManager { static const Color grayColor = Color(0xFF999999); static const Color red = Color(0xFFFF0000); static const Color graysColor = Color(0xffEBEBEB); + static const Color lightGrayColor = Color(0xB2999999); + static const Color grayBorder = Color(0xFFCFCFCF); static const Color textGray = Color(0xffD5D5D5); static const Color btnColor = Color(0xFF00008B); static const Color blueColor = Color(0xFF0036E6); @@ -41,6 +45,14 @@ abstract class ColorsManager { static const Color blue4 = Color(0xFF001E7E); static const Color textGreen = Color(0xFF008905); static const Color yaGreen = Color(0xFFFFBF44); + static const Color checkBoxFillColor = Color(0xFFF3F3F3); + static const Color semiTransparentBlackColor = Color(0x3F000000); + static const Color transparentColor = Color(0x00000000); + static const Color spaceColor = Color(0xB2023DFE); + static const Color counterBackgroundColor = Color(0xCCF4F4F4); + static const Color neutralGray = Color(0xFFE5E5E5); + static const Color warningRed = Color(0xFFFF6465); + static const Color borderColor = Color(0xFFE5E5E5); static const Color CircleImageBackground = Color(0xFFF4F4F4); } //background: #background: #5D5D5D; diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index dcf6b367..bb37fd60 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -11,14 +11,12 @@ abstract class ApiEndpoints { static const String visitorPassword = '/visitor-password'; static const String getDevices = '/visitor-password/devices'; - static const String sendOnlineOneTime = - '/visitor-password/temporary-password/online/one-time'; + static const String sendOnlineOneTime = '/visitor-password/temporary-password/online/one-time'; static const String sendOnlineMultipleTime = '/visitor-password/temporary-password/online/multiple-time'; //offline Password - static const String sendOffLineOneTime = - '/visitor-password/temporary-password/offline/one-time'; + static const String sendOffLineOneTime = '/visitor-password/temporary-password/offline/one-time'; static const String sendOffLineMultipleTime = '/visitor-password/temporary-password/offline/multiple-time'; @@ -36,30 +34,44 @@ abstract class ApiEndpoints { static const String openDoorLock = '/door-lock/open/{doorLockUuid}'; static const String getDeviceLogs = '/device/report-logs/{uuid}?code={code}'; + +// Space Module + static const String createSpace = '/communities/{communityId}/spaces'; + static const String listSpaces = '/communities/{communityId}/spaces'; + static const String deleteSpace = '/communities/{communityId}/spaces/{spaceId}'; + static const String updateSpace = '/communities/{communityId}/spaces/{spaceId}'; + static const String getSpace = '/communities/{communityId}/spaces/{spaceId}'; + static const String getSpaceHierarchy = '/communities/{communityId}/spaces'; + +// Community Module + static const String createCommunity = '/communities'; + static const String getCommunityList = '/communities'; + static const String getCommunityById = '/communities/{communityId}'; + static const String updateCommunity = '/communities/{communityId}'; + static const String deleteCommunity = '/communities/{communityId}'; + static const String getUserCommunities = '/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 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'; + static const String powerClamp = '/device/{powerClampUuid}/power-clamp/status'; + + //product + static const String listProducts = '/products'; static const String getSpaceScenes = '/scene/tap-to-run/{unitUuid}'; static const String getSpaceAutomation = '/automation/{unitUuid}'; static const String getIconScene = '/scene/icon'; static const String createScene = '/scene/tap-to-run'; static const String createAutomation = '/automation'; - static const String getUnitScenes = - '/communities/{communityUuid}/spaces/{spaceUuid}/scenes'; - static const String getAutomationDetails = - '/automation/details/{automationId}'; + static const String getUnitScenes = '/communities/{communityUuid}/spaces/{spaceUuid}/scenes'; + static const String getAutomationDetails = '/automation/details/{automationId}'; static const String getScene = '/scene/tap-to-run/{sceneId}'; - static const String deleteScene = '/scene/tap-to-run/{sceneId}'; + static const String deleteScene = '/scene/tap-to-run/{sceneId}'; static const String deleteAutomation = '/automation/{automationId}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index cffaf472..958c2c1c 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -13,12 +13,10 @@ 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 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"; + static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; static const String devicesIcon = "assets/images/devices_icon.svg"; static const String moveinIcon = "assets/images/movein_icon.svg"; static const String constructionIcon = "assets/images/construction_icon.svg"; @@ -31,15 +29,13 @@ class Assets { static const String emptyTable = "assets/images/empty_table.svg"; // General assets - static const String motionlessDetection = - "assets/icons/motionless_detection.svg"; + static const String motionlessDetection = "assets/icons/motionless_detection.svg"; static const String acHeating = "assets/icons/ac_heating.svg"; static const String acPowerOff = "assets/icons/ac_power_off.svg"; static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; static const String switchAlarmSound = "assets/icons/switch_alarm_sound.svg"; static const String resetOff = "assets/icons/reset_off.svg"; - static const String sensitivityOperationIcon = - "assets/icons/sesitivity_operation_icon.svg"; + static const String sensitivityOperationIcon = "assets/icons/sesitivity_operation_icon.svg"; static const String motionDetection = "assets/icons/motion_detection.svg"; static const String freezing = "assets/icons/freezing.svg"; static const String indicator = "assets/icons/indicator.svg"; @@ -60,8 +56,7 @@ class Assets { static const String celsiusDegrees = "assets/icons/celsius_degrees.svg"; static const String masterState = "assets/icons/master_state.svg"; static const String acPower = "assets/icons/ac_power.svg"; - static const String farDetectionFunction = - "assets/icons/far_detection_function.svg"; + static const String farDetectionFunction = "assets/icons/far_detection_function.svg"; static const String nobodyTime = "assets/icons/nobody_time.svg"; // Automation functions @@ -69,47 +64,33 @@ class Assets { "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 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 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 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 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"; // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; - static const String sensorPresenceIcon = - "assets/icons/sensor_presence_ic.svg"; + static const String sensorPresenceIcon = "assets/icons/sensor_presence_ic.svg"; static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; - static const String illuminanceRecordIcon = - "assets/icons/illuminance_record_ic.svg"; - static const String presenceRecordIcon = - "assets/icons/presence_record_ic.svg"; - static const String helpDescriptionIcon = - "assets/icons/help_description_ic.svg"; + static const String illuminanceRecordIcon = "assets/icons/illuminance_record_ic.svg"; + static const String presenceRecordIcon = "assets/icons/presence_record_ic.svg"; + static const String helpDescriptionIcon = "assets/icons/help_description_ic.svg"; static const String lightPulp = "assets/icons/light_pulb.svg"; static const String acDevice = "assets/icons/ac_device.svg"; @@ -142,7 +123,34 @@ class Assets { static const String dyi = 'assets/icons/dyi.svg'; static const String office = 'assets/icons/office.svg'; static const String parlour = 'assets/icons/parlour.svg'; - static const String grid = "assets/images/grid.svg"; + static const String grid = 'assets/images/grid.svg'; + + static const String bbq = 'assets/icons/bbq_icon.svg'; + static const String building = 'assets/icons/building_icon.svg'; + static const String desk = 'assets/icons/desk_icon.svg'; + static const String door = 'assets/icons/door_icon.svg'; + static const String gym = 'assets/icons/gym_icon.svg'; + static const String location = 'assets/icons/location_icon.svg'; + static const String parking = 'assets/icons/parking_icon.svg'; + static const String pool = 'assets/icons/pool_icon.svg'; + static const String sauna = 'assets/icons/sauna_icon.svg'; + static const String stair = 'assets/icons/stair_icon.svg'; + static const String steamRoom = 'assets/icons/steam_room_icon.svg'; + static const String street = 'assets/icons/street_icon.svg'; + static const String unit = 'assets/icons/unit_icon.svg'; + static const String villa = 'assets/icons/villa_icon.svg'; + static const String iconEdit = 'assets/icons/icon_edit_icon.svg'; + static const String textFieldSearch = 'assets/icons/textfield_search_icon.svg'; + static const String roundedAddIcon = 'assets/icons/rounded_add_icon.svg'; + static const String addIcon = 'assets/icons/add_icon.svg'; + static const String smartThermostatIcon = 'assets/icons/smart_thermostat_icon.svg'; + static const String smartLightIcon = 'assets/icons/smart_light_icon.svg'; + static const String presenceSensor = 'assets/icons/presence_sensor.svg'; + static const String Gang3SwitchIcon = 'assets/icons/3_Gang_switch_icon.svg'; + static const String Gang2SwitchIcon = 'assets/icons/2_Gang_Switch_icon.svg'; + 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'; @@ -183,8 +191,7 @@ class Assets { //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'; + static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -219,6 +226,17 @@ class Assets { //assets/icons/sos_normal.svg static const String sosNormal = 'assets/icons/sos_normal.svg'; + static const String waterLeakSensor = 'assets/icons/water_leak_sensor.svg'; + static const String powerClamp = 'assets/icons/power_clamp.svg'; + static const String threeTouchSwitch = 'assets/icons/3G_touch_switch.svg'; + static const String twoTouchSwitch = 'assets/icons/2G_touch_switch.svg'; + static const String oneTouchSwitch = 'assets/icons/1G_touch_switch.svg'; + + static const String garageDoor = 'assets/icons/garage_opener.svg'; + static const String doorSensor = 'assets/icons/door_sensor.svg'; + + static const String delete = 'assets/icons/delete.svg'; + static const String edit = 'assets/icons/edit.svg'; //assets/icons/routine/tab_to_run.svg static const String tabToRun = 'assets/icons/routine/tab_to_run.svg'; @@ -238,64 +256,40 @@ class Assets { static const String delay = 'assets/icons/routine/delay.svg'; // Assets for functions_icons - static const String assetsSensitivityFunction = - "assets/icons/functions_icons/sensitivity.svg"; + static const String assetsSensitivityFunction = "assets/icons/functions_icons/sensitivity.svg"; static const String assetsSensitivityOperationIcon = "assets/icons/functions_icons/sesitivity_operation_icon.svg"; - static const String assetsAcPower = - "assets/icons/functions_icons/ac_power.svg"; - static const String assetsAcPowerOFF = - "assets/icons/functions_icons/ac_power_off.svg"; - static const String assetsChildLock = - "assets/icons/functions_icons/child_lock.svg"; - static const String assetsFreezing = - "assets/icons/functions_icons/freezing.svg"; - static const String assetsFanSpeed = - "assets/icons/functions_icons/fan_speed.svg"; - static const String assetsAcCooling = - "assets/icons/functions_icons/ac_cooling.svg"; - static const String assetsAcHeating = - "assets/icons/functions_icons/ac_heating.svg"; - static const String assetsCelsiusDegrees = - "assets/icons/functions_icons/celsius_degrees.svg"; - static const String assetsTempreture = - "assets/icons/functions_icons/tempreture.svg"; - static const String assetsAcFanLow = - "assets/icons/functions_icons/ac_fan_low.svg"; - static const String assetsAcFanMiddle = - "assets/icons/functions_icons/ac_fan_middle.svg"; - static const String assetsAcFanHigh = - "assets/icons/functions_icons/ac_fan_high.svg"; - static const String assetsAcFanAuto = - "assets/icons/functions_icons/ac_fan_auto.svg"; - static const String assetsSceneChildLock = - "assets/icons/functions_icons/scene_child_lock.svg"; + static const String assetsAcPower = "assets/icons/functions_icons/ac_power.svg"; + static const String assetsAcPowerOFF = "assets/icons/functions_icons/ac_power_off.svg"; + static const String assetsChildLock = "assets/icons/functions_icons/child_lock.svg"; + static const String assetsFreezing = "assets/icons/functions_icons/freezing.svg"; + static const String assetsFanSpeed = "assets/icons/functions_icons/fan_speed.svg"; + static const String assetsAcCooling = "assets/icons/functions_icons/ac_cooling.svg"; + static const String assetsAcHeating = "assets/icons/functions_icons/ac_heating.svg"; + static const String assetsCelsiusDegrees = "assets/icons/functions_icons/celsius_degrees.svg"; + static const String assetsTempreture = "assets/icons/functions_icons/tempreture.svg"; + static const String assetsAcFanLow = "assets/icons/functions_icons/ac_fan_low.svg"; + static const String assetsAcFanMiddle = "assets/icons/functions_icons/ac_fan_middle.svg"; + static const String assetsAcFanHigh = "assets/icons/functions_icons/ac_fan_high.svg"; + static const String assetsAcFanAuto = "assets/icons/functions_icons/ac_fan_auto.svg"; + static const String assetsSceneChildLock = "assets/icons/functions_icons/scene_child_lock.svg"; static const String assetsSceneChildUnlock = "assets/icons/functions_icons/scene_child_unlock.svg"; - static const String assetsSceneRefresh = - "assets/icons/functions_icons/scene_refresh.svg"; - static const String assetsLightCountdown = - "assets/icons/functions_icons/light_countdown.svg"; - static const String assetsFarDetection = - "assets/icons/functions_icons/far_detection.svg"; + static const String assetsSceneRefresh = "assets/icons/functions_icons/scene_refresh.svg"; + static const String assetsLightCountdown = "assets/icons/functions_icons/light_countdown.svg"; + static const String assetsFarDetection = "assets/icons/functions_icons/far_detection.svg"; static const String assetsFarDetectionFunction = "assets/icons/functions_icons/far_detection_function.svg"; - static const String assetsIndicator = - "assets/icons/functions_icons/indicator.svg"; - static const String assetsMotionDetection = - "assets/icons/functions_icons/motion_detection.svg"; + static const String assetsIndicator = "assets/icons/functions_icons/indicator.svg"; + static const String assetsMotionDetection = "assets/icons/functions_icons/motion_detection.svg"; static const String assetsMotionlessDetection = "assets/icons/functions_icons/motionless_detection.svg"; - static const String assetsNobodyTime = - "assets/icons/functions_icons/nobody_time.svg"; - static const String assetsFactoryReset = - "assets/icons/functions_icons/factory_reset.svg"; - static const String assetsMasterState = - "assets/icons/functions_icons/master_state.svg"; + static const String assetsNobodyTime = "assets/icons/functions_icons/nobody_time.svg"; + static const String assetsFactoryReset = "assets/icons/functions_icons/factory_reset.svg"; + static const String assetsMasterState = "assets/icons/functions_icons/master_state.svg"; static const String assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; - static const String assetsResetOff = - "assets/icons/functions_icons/reset_off.svg"; + static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; // Assets for automation_functions static const String assetsCardUnlock = @@ -326,8 +320,7 @@ class Assets { "assets/icons/functions_icons/automation_functions/self_test_result.svg"; static const String assetsPresence = "assets/icons/functions_icons/automation_functions/presence.svg"; - static const String assetsMotion = - "assets/icons/functions_icons/automation_functions/motion.svg"; + static const String assetsMotion = "assets/icons/functions_icons/automation_functions/motion.svg"; static const String assetsCurrentTemp = "assets/icons/functions_icons/automation_functions/current_temp.svg"; static const String assetsPresenceState = diff --git a/lib/utils/constants/routes_const.dart b/lib/utils/constants/routes_const.dart index 9f3b2d2b..094787d4 100644 --- a/lib/utils/constants/routes_const.dart +++ b/lib/utils/constants/routes_const.dart @@ -4,4 +4,5 @@ class RoutesConst { static const String visitorPassword = '/visitor-password'; static const String accessManagementPage = '/access-management-page'; static const String deviceManagementPage = '/device-management-page'; + static const String spacesManagementPage = '/spaces_management-page'; } diff --git a/lib/utils/constants/space_icon_const.dart b/lib/utils/constants/space_icon_const.dart new file mode 100644 index 00000000..5d141a53 --- /dev/null +++ b/lib/utils/constants/space_icon_const.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + +const List spaceIconList = [ + Assets.location, + Assets.villa, + Assets.gym, + Assets.sauna, + Assets.bbq, + Assets.building, + Assets.desk, + Assets.door, + Assets.parking, + Assets.pool, + Assets.stair, + Assets.steamRoom, + Assets.street, + Assets.unit, +]; diff --git a/lib/utils/style.dart b/lib/utils/style.dart index 04108333..a80c68d6 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -21,11 +21,11 @@ InputDecoration? textBoxDecoration({bool suffixIcon = false}) => InputDecoration borderSide: BorderSide.none, // Remove the underline ), errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red, width: 2), + borderSide: const BorderSide(color: Colors.red, width: 2), borderRadius: BorderRadius.circular(8), ), focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red, width: 2), + borderSide: const BorderSide(color: Colors.red, width: 2), borderRadius: BorderRadius.circular(8), ), ); @@ -47,3 +47,15 @@ BoxDecoration containerWhiteDecoration = BoxDecoration(boxShadow: [ offset: const Offset(0, 5), // changes position of shadow ), ], color: ColorsManager.whiteColors, borderRadius: const BorderRadius.all(Radius.circular(15))); + +BoxDecoration subSectionContainerDecoration = BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 1, + offset: const Offset(0, 2), + ), + ], +); diff --git a/pubspec.lock b/pubspec.lock index cfd8b310..192106d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3aa81860..ffff62ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,8 +50,8 @@ dependencies: dropdown_search: ^5.0.6 flutter_dotenv: ^5.1.0 fl_chart: ^0.69.0 + uuid: ^4.4.2 time_picker_spinner: ^1.0.0 - uuid: ^4.4.0 dev_dependencies: flutter_test: