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