added bloc for create community

This commit is contained in:
hannathkadher
2024-12-04 11:05:46 +04:00
parent 0b628c85a5
commit 9bddd151bb
38 changed files with 297 additions and 271 deletions

View File

@ -0,0 +1,376 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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<SpaceManagementEvent, SpaceManagementState> {
final CommunitySpaceManagementApi _api;
final ProductApi _productApi;
List<ProductModel>? _cachedProducts;
SpaceManagementBloc(this._api, this._productApi)
: super(SpaceManagementInitial()) {
on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces);
on<UpdateSpacePositionEvent>(_onUpdateSpacePosition);
on<CreateCommunityEvent>(_onCreateCommunity);
on<SelectCommunityEvent>(_onSelectCommunity);
on<DeleteCommunityEvent>(_onCommunityDelete);
on<UpdateCommunityEvent>(_onUpdateCommunity);
on<SaveSpacesEvent>(_onSaveSpaces);
on<FetchProductsEvent>(_onFetchProducts);
on<SelectSpaceEvent>(_onSelectSpace);
on<NewCommunityEvent>(_onNewCommunity);
}
void _onUpdateCommunity(
UpdateCommunityEvent event,
Emitter<SpaceManagementState> 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<CommunityModel>.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 _onloadProducts() async {
if (_cachedProducts == null) {
final products = await _productApi.fetchProducts();
_cachedProducts = products;
}
}
void _onFetchProducts(
FetchProductsEvent event,
Emitter<SpaceManagementState> emit,
) async {
try {
_onloadProducts();
} catch (e) {
emit(SpaceManagementError('Error fetching products: $e'));
}
}
Future<List<SpaceModel>> _fetchSpacesForCommunity(
String communityUuid) async {
return await _api.getSpaceHierarchy(communityUuid);
}
void _onNewCommunity(
NewCommunityEvent event,
Emitter<SpaceManagementState> emit,
) {
try {
if (event.communities.isEmpty) {
emit(const SpaceManagementError('No communities provided.'));
return;
}
emit(BlankState(
communities: event.communities,
products: _cachedProducts ?? [],
));
} catch (error) {
emit(SpaceManagementError('Error loading communities: $error'));
}
}
void _onLoadCommunityAndSpaces(
LoadCommunityAndSpacesEvent event,
Emitter<SpaceManagementState> emit,
) async {
emit(SpaceManagementLoading());
try {
_onloadProducts();
List<CommunityModel> communities = await _api.fetchCommunities();
List<CommunityModel> updatedCommunities = await Future.wait(
communities.map((community) async {
List<SpaceModel> spaces =
await _fetchSpacesForCommunity(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<SpaceManagementState> 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<SpaceManagementState> emit,
) {}
void _onCreateCommunity(
CreateCommunityEvent event,
Emitter<SpaceManagementState> emit,
) async {
final previousState = state;
emit(SpaceManagementLoading());
try {
CommunityModel? newCommunity =
await _api.createCommunity(event.name, event.description);
if (newCommunity != null) {
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
final prevCommunities = List<CommunityModel>.from(
(previousState as dynamic).communities,
);
final updatedCommunities = prevCommunities..add(newCommunity);
emit(SpaceManagementLoaded(
communities: updatedCommunities,
products: _cachedProducts ?? [],
selectedCommunity: newCommunity,
selectedSpace: null));
}
} else {
emit(const SpaceManagementError('Error creating community'));
}
} catch (e) {
emit(SpaceManagementError('Error creating community: $e'));
}
}
void _onSelectCommunity(
SelectCommunityEvent event,
Emitter<SpaceManagementState> emit,
) async {
_handleCommunitySpaceStateUpdate(
emit: emit,
selectedCommunity: event.selectedCommunity,
selectedSpace: null,
);
}
void _onSelectSpace(
SelectSpaceEvent event,
Emitter<SpaceManagementState> emit,
) {
_handleCommunitySpaceStateUpdate(
emit: emit,
selectedCommunity: event.selectedCommunity,
selectedSpace: event.selectedSpace,
);
}
void _handleCommunitySpaceStateUpdate({
required Emitter<SpaceManagementState> emit,
CommunityModel? selectedCommunity,
SpaceModel? selectedSpace,
}) {
final previousState = state;
emit(SpaceManagementLoading());
try {
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
final communities = List<CommunityModel>.from(
(previousState as dynamic).communities,
);
emit(SpaceManagementLoaded(
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
));
}
} catch (e) {
emit(SpaceManagementError('Error updating state: $e'));
}
}
void _onSaveSpaces(
SaveSpacesEvent event,
Emitter<SpaceManagementState> emit,
) async {
final previousState = state;
emit(SpaceManagementLoading());
try {
final updatedSpaces =
await saveSpacesHierarchically(event.spaces, event.communityUuid);
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
emit(SpaceCreationSuccess(spaces: updatedSpaces));
if (previousState is SpaceManagementLoaded) {
_updateLoadedState(
previousState,
allSpaces,
event.communityUuid,
emit,
);
} else {
add(LoadCommunityAndSpacesEvent());
}
} catch (e) {
emit(SpaceManagementError('Error saving spaces: $e'));
if (previousState is SpaceManagementLoaded) {
emit(previousState);
}
}
}
void _updateLoadedState(
SpaceManagementLoaded previousState,
List<SpaceModel> allSpaces,
String communityUuid,
Emitter<SpaceManagementState> emit,
) {
final communities = List<CommunityModel>.from(previousState.communities);
for (var community in communities) {
if (community.uuid == communityUuid) {
community.spaces = allSpaces;
emit(SpaceManagementLoaded(
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: community,
selectedSpace: null,
));
return;
}
}
}
Future<List<SpaceModel>> saveSpacesHierarchically(
List<SpaceModel> 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 {
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<SpaceModel> flattenHierarchy(List<SpaceModel> spaces) {
final result = <SpaceModel>{};
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
}
}

View File

@ -0,0 +1,142 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; // Import for Offset
abstract class SpaceManagementEvent extends Equatable {
const SpaceManagementEvent();
@override
List<Object> get props => [];
}
class LoadCommunityAndSpacesEvent extends SpaceManagementEvent {}
class DeleteCommunityEvent extends SpaceManagementEvent {
final String communityUuid;
const DeleteCommunityEvent({
required this.communityUuid,
});
@override
List<Object> 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<Object> 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<SpaceModel> spaces;
final String communityUuid;
const SaveSpacesEvent({
required this.spaces,
required this.communityUuid,
});
@override
List<Object> get props => [spaces, communityUuid];
}
class UpdateSpacePositionEvent extends SpaceManagementEvent {
final int index;
final Offset newPosition;
const UpdateSpacePositionEvent(this.index, this.newPosition);
@override
List<Object> get props => [index, newPosition];
}
class CreateCommunityEvent extends SpaceManagementEvent {
final String name;
final String description;
const CreateCommunityEvent({
required this.name,
required this.description,
});
@override
List<Object> get props => [name, description];
}
class UpdateCommunityEvent extends SpaceManagementEvent {
final String communityUuid;
final String name;
const UpdateCommunityEvent({
required this.communityUuid,
required this.name,
});
@override
List<Object> get props => [communityUuid, name];
}
class SelectCommunityEvent extends SpaceManagementEvent {
final CommunityModel? selectedCommunity;
const SelectCommunityEvent({
required this.selectedCommunity,
});
@override
List<Object> get props => [];
}
class NewCommunityEvent extends SpaceManagementEvent {
final List<CommunityModel> communities;
const NewCommunityEvent({required this.communities});
@override
List<Object> get props => [communities];
}
class SelectSpaceEvent extends SpaceManagementEvent {
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
const SelectSpaceEvent({
required this.selectedCommunity,
required this.selectedSpace,
});
@override
List<Object> get props => [];
}
class FetchProductsEvent extends SpaceManagementEvent {}
class LoadSpaceHierarchyEvent extends SpaceManagementEvent {
final String communityId;
const LoadSpaceHierarchyEvent({required this.communityId});
@override
List<Object> get props => [communityId];
}

View File

@ -0,0 +1,56 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
abstract class SpaceManagementState extends Equatable {
const SpaceManagementState();
@override
List<Object> get props => [];
}
class SpaceManagementInitial extends SpaceManagementState {}
class SpaceManagementLoading extends SpaceManagementState {}
class SpaceManagementLoaded extends SpaceManagementState {
final List<CommunityModel> communities;
final List<ProductModel> products;
CommunityModel? selectedCommunity;
SpaceModel? selectedSpace;
SpaceManagementLoaded(
{required this.communities,
required this.products,
this.selectedCommunity,
this.selectedSpace});
}
class BlankState extends SpaceManagementState {
final List<CommunityModel> communities;
final List<ProductModel> products;
BlankState({
required this.communities,
required this.products,
});
}
class SpaceCreationSuccess extends SpaceManagementState {
final List<SpaceModel> spaces;
const SpaceCreationSuccess({required this.spaces});
@override
List<Object> get props => [spaces];
}
class SpaceManagementError extends SpaceManagementState {
final String errorMessage;
const SpaceManagementError(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@ -0,0 +1,48 @@
import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
class CommunityModel {
final String uuid;
final DateTime createdAt;
final DateTime updatedAt;
String name;
final String description;
final RegionModel? region;
List<SpaceModel> 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<String, dynamic> 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<String, dynamic> 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
};
}
}

View File

@ -0,0 +1,25 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/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<String, dynamic> 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<String, dynamic> map, Map<String, SpaceModel> spaces) {
return Connection(
startSpace: spaces[map['startUuid']]!,
endSpace: spaces[map['endUuid']]!,
direction: map['direction'],
);
}
}

View File

@ -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<String, dynamic> 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<String, dynamic> 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)';
}
}

View File

@ -0,0 +1,18 @@
class SelectedProduct {
final String productId;
int count;
SelectedProduct({required this.productId, required this.count});
Map<String, dynamic> toJson() {
return {
'productId': productId,
'count': count,
};
}
@override
String toString() {
return 'SelectedProduct(productId: $productId, count: $count)';
}
}

View File

@ -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,
});
}

View File

@ -0,0 +1,134 @@
import 'dart:ui';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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<SpaceModel> children;
Offset position;
bool isHovered;
SpaceStatus status;
List<SelectedProduct> selectedProducts;
String internalId;
List<Connection> 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<String, dynamic> json,
{String? parentInternalId}) {
final String internalId = json['internalId'] ?? const Uuid().v4();
final List<SpaceModel> children = json['children'] != null
? (json['children'] as List).map((childJson) {
return SpaceModel.fromJson(
childJson,
parentInternalId: internalId,
);
}).toList()
: [];
final instance = 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()
: [],
);
if (json['incomingConnections'] != null &&
json['incomingConnections'] is List &&
(json['incomingConnections'] as List).isNotEmpty &&
instance.parent != null) {
final conn = json['incomingConnections'][0];
instance.incomingConnection = Connection(
startSpace: instance.parent ?? instance, // Parent space
endSpace: instance, // This space instance
direction: conn['direction'],
);
}
return instance;
}
Map<String, dynamic> 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);
}
}

View File

@ -0,0 +1,39 @@
import 'space_model.dart';
class SpacesResponse {
final List<SpaceModel> 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<String, dynamic> 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'],
);
}
}

View File

@ -0,0 +1,70 @@
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/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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<SpaceManagementPage> {
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
final ProductApi _productApi = ProductApi();
Map<String, List<SpaceModel>> communitySpaces = {};
List<ProductModel> 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<SpaceManagementBloc, SpaceManagementState>(
builder: (context, state) {
if (state is SpaceManagementLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is BlankState) {
return LoadedSpaceView(
communities: state.communities,
selectedCommunity: null,
selectedSpace: null,
products: state.products,
);
} else if (state is SpaceManagementLoaded) {
return LoadedSpaceView(
communities: state.communities,
selectedCommunity: state.selectedCommunity,
selectedSpace: state.selectedSpace,
products: state.products,
);
} else if (state is SpaceManagementError) {
return Center(child: Text('Error: ${state.errorMessage}'));
}
return Container();
}),
),
);
}
}

View File

@ -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/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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<ProductModel>? products;
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
final List<SelectedProduct>? initialSelectedProducts;
const AddDeviceWidget({
super.key,
this.products,
this.initialSelectedProducts,
this.onProductsSelected,
});
@override
_AddDeviceWidgetState createState() => _AddDeviceWidgetState();
}
class _AddDeviceWidgetState extends State<AddDeviceWidget> {
late final ScrollController _scrollController;
late List<SelectedProduct> 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),
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class BlankCommunityWidget extends StatefulWidget {
final List<CommunityModel> communities;
BlankCommunityWidget({required this.communities});
@override
_BlankCommunityWidgetState createState() => _BlankCommunityWidgetState();
}
class _BlankCommunityWidgetState extends State<BlankCommunityWidget> {
@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: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 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: const BorderSide(
width: 4,
strokeAlign: BorderSide.strokeAlignOutside,
color: ColorsManager.borderColor,
),
borderRadius: BorderRadius.circular(5),
),
),
),
),
),
const SizedBox(height: 9),
Text('Blank',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor,
)),
],
));
},
),
),
);
}
void _showCreateCommunityDialog(BuildContext parentContext) {
showDialog(
context: parentContext,
builder: (context) => CreateCommunityDialog(
isEditMode: false,
existingCommunityNames: widget.communities.map((community) => community.name).toList(),
onCreateCommunity: (String communityName, String description) {
parentContext.read<SpaceManagementBloc>().add(
CreateCommunityEvent(
name: communityName,
description: description,
),
);
},
),
);
}
}

View File

@ -0,0 +1,201 @@
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/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatefulWidget {
final String? communityName;
final bool isEditingName;
final bool isSave;
final TextEditingController nameController;
final VoidCallback onSave;
final VoidCallback onDelete;
final VoidCallback onEditName;
final ValueChanged<String> onNameSubmitted;
final List<CommunityModel> communities;
final CommunityModel? community;
const CommunityStructureHeader(
{super.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,
this.community,
required this.communities});
@override
State<CommunityStructureHeader> createState() =>
_CommunityStructureHeaderState();
}
class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
@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),
],
),
],
),
);
}
void _showCreateCommunityDialog(BuildContext parentContext) {
showDialog(
context: parentContext,
builder: (context) => CreateCommunityDialog(
isEditMode: true,
existingCommunityNames:
widget.communities.map((community) => community.name).toList(),
initialName: widget.community?.name ?? '',
onCreateCommunity: (String communityName, String description) {
widget.onNameSubmitted(communityName);
},
),
);
}
Widget _buildCommunityInfo(ThemeData theme, double screenWidth) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Community Structure',
style: theme.textTheme.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
),
if (widget.communityName != null)
Row(
children: [
Expanded(
child: Row(
children: [
if (!widget.isEditingName)
Flexible(
child: Text(
widget.communityName!,
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
if (widget.isEditingName)
SizedBox(
width: screenWidth * 0.1,
child: TextField(
controller: widget.nameController,
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
),
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
onSubmitted: widget.onNameSubmitted,
),
),
const SizedBox(width: 2),
GestureDetector(
onTap: () => _showCreateCommunityDialog(context),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
],
),
),
if (widget.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: widget.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,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,532 @@
// 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/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CommunityStructureArea extends StatefulWidget {
final CommunityModel? selectedCommunity;
SpaceModel? selectedSpace;
final List<ProductModel>? products;
final ValueChanged<SpaceModel?>? onSpaceSelected;
final List<CommunityModel> communities;
final List<SpaceModel> spaces;
CommunityStructureArea({
this.selectedCommunity,
this.selectedSpace,
required this.communities,
this.products,
required this.spaces,
this.onSpaceSelected,
});
@override
_CommunityStructureAreaState createState() => _CommunityStructureAreaState();
}
class _CommunityStructureAreaState extends State<CommunityStructureArea> {
double canvasWidth = 1000;
double canvasHeight = 1000;
List<SpaceModel> spaces = [];
List<Connection> 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(
communities: widget.communities,
);
}
Size screenSize = MediaQuery.of(context).size;
return Expanded(
child: GestureDetector(
onTap: () {
_deselectSpace(context);
},
child: Container(
decoration: const BoxDecoration(
border: Border(
left: BorderSide(color: ColorsManager.whiteColors, width: 1.0),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommunityStructureHeader(
communities: widget.communities,
communityName: widget.selectedCommunity?.name,
community: widget.selectedCommunity,
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<SpaceManagementBloc>().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(context, 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<SelectedProduct> 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,
editSpace: space,
isEdit: true,
selectedProducts: space.selectedProducts,
onCreateSpace: (String name, String icon,
List<SelectedProduct> 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
}
});
},
key: Key(space.name),
);
},
);
}
void _handleHoverChanged(int index, bool isHovered) {
setState(() {
spaces[index].isHovered = isHovered;
});
}
List<SpaceModel> flattenSpaces(List<SpaceModel> spaces) {
List<SpaceModel> 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<Connection> createConnections(List<SpaceModel> spaces) {
List<Connection> 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<SpaceModel> 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<SpaceManagementBloc>().add(SaveSpacesEvent(
spaces: spacesToSave,
communityUuid: communityUuid,
));
}
void _onDelete() {
if (widget.selectedCommunity != null &&
widget.selectedCommunity?.uuid != null &&
widget.selectedSpace == null) {
context.read<SpaceManagementBloc>().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(BuildContext context, SpaceModel space) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity,
selectedSpace: space),
);
}
bool _isHighlightedSpace(SpaceModel space) {
final selectedSpace = widget.selectedSpace;
if (selectedSpace == null) return true;
return space == selectedSpace ||
selectedSpace.parent?.internalId == space.internalId ||
selectedSpace.children
?.any((child) => child.internalId == space.internalId) ==
true;
}
void _deselectSpace(BuildContext context) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity, selectedSpace: null),
);
}
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<SpaceModel> spaces) {
return spaces.isNotEmpty &&
spaces.any((space) =>
space.status == SpaceStatus.newSpace ||
space.status == SpaceStatus.modified ||
space.status == SpaceStatus.deleted);
}
}

View File

@ -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<Widget>? 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 ?? [],
);
}
}

View File

@ -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<int> onCountChanged;
const CounterWidget({
Key? key,
this.initialCount = 0,
required this.onCountChanged,
}) : super(key: key);
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
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,
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CurvedLinePainter extends CustomPainter {
final List<Connection> 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;
}
}

View File

@ -0,0 +1,408 @@
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/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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<SelectedProduct> selectedProducts)
onCreateSpace;
final List<ProductModel>? products;
final String? name;
final String? icon;
final bool isEdit;
final List<SelectedProduct> selectedProducts;
final SpaceModel? parentSpace;
final SpaceModel? editSpace;
const CreateSpaceDialog(
{super.key,
this.parentSpace,
required this.onCreateSpace,
this.products,
this.name,
this.icon,
this.isEdit = false,
this.editSpace,
this.selectedProducts = const []});
@override
CreateSpaceDialogState createState() => CreateSpaceDialogState();
}
class CreateSpaceDialogState extends State<CreateSpaceDialog> {
String selectedIcon = Assets.location;
String enteredName = '';
List<SelectedProduct> 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) ||
(widget.parentSpace?.name == value) ||
(widget.editSpace?.children.any(
(child) => child.name == value) ??
false)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
isNameFieldExist = false;
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<ProductModel> 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<ProductModel> 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;
}
}

View File

@ -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),
);
}

View File

@ -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<String> 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,
),
);
},
),
),
),
),
);
}
}

View File

@ -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,
],
),
),
),
);
}
}

View File

@ -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<HoverableButton> createState() => _HoverableButtonState();
}
class _HoverableButtonState extends State<HoverableButton> {
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);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/gradient_canvas_border_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart';
class LoadedSpaceView extends StatefulWidget {
final List<CommunityModel> communities;
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
final List<ProductModel>? products;
const LoadedSpaceView({
super.key,
required this.communities,
this.selectedCommunity,
this.selectedSpace,
this.products,
});
@override
_LoadedStateViewState createState() => _LoadedStateViewState();
}
class _LoadedStateViewState extends State<LoadedSpaceView> {
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Row(
children: [
SidebarWidget(
communities: widget.communities,
selectedSpaceUuid: widget.selectedSpace?.uuid ??
widget.selectedCommunity?.uuid ??
'',
),
CommunityStructureArea(
selectedCommunity: widget.selectedCommunity,
selectedSpace: widget.selectedSpace,
spaces: widget.selectedCommunity?.spaces ?? [],
products: widget.products,
communities: widget.communities,
),
],
),
const GradientCanvasBorderWidget(),
],
);
}
SpaceModel? findSpaceByUuid(String? uuid, List<CommunityModel> communities) {
for (var community in communities) {
for (var space in community.spaces) {
if (space.uuid == uuid) return space;
}
}
return null;
}
}

View File

@ -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),
),
),
);
}
}

View File

@ -0,0 +1,234 @@
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/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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 List<CommunityModel> communities;
final String? selectedSpaceUuid;
const SidebarWidget({
super.key,
required this.communities,
this.selectedSpaceUuid,
});
@override
_SidebarWidgetState createState() => _SidebarWidgetState();
}
class _SidebarWidgetState extends State<SidebarWidget> {
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;
});
}
}
// Function to filter communities based on the search query
List<CommunityModel> _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: () => _navigateToBlank(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(context, community);
}).toList(),
),
),
],
),
);
}
void _navigateToBlank(BuildContext context) {
setState(() {
_selectedId = '';
});
context.read<SpaceManagementBloc>().add(
NewCommunityEvent(communities: widget.communities),
);
}
Widget _buildCommunityTile(BuildContext context, 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
});
context.read<SpaceManagementBloc>().add(
SelectCommunityEvent(selectedCommunity: 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);
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;
});
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(
selectedCommunity: community, selectedSpace: space),
);
},
children: space.children.isNotEmpty
? space.children
.map((childSpace) => _buildSpaceTile(childSpace, community))
.toList()
: [], // Recursively render child spaces if available
);
}
void _handleExpansionChange(String uuid, bool expanded) {}
}

View File

@ -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<Offset> 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,
),
],
],
),
),
);
}
}

View File

@ -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
),
],
);
}
}

View File

@ -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<bool> onExpansionChanged;
final List<Widget>? 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<SpaceTile> {
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 ?? [],
);
}
}

View File

@ -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)),
],
),
),
),
);
}
}