Merged with dev

This commit is contained in:
Abdullah Alassaf
2025-04-06 01:17:30 +03:00
13 changed files with 481 additions and 319 deletions

View File

@ -2,14 +2,17 @@ import 'package:flutter/foundation.dart';
class FactoryResetModel { class FactoryResetModel {
final List<String> devicesUuid; final List<String> devicesUuid;
final String operationType;
FactoryResetModel({ FactoryResetModel({
required this.devicesUuid, required this.devicesUuid,
this.operationType = "RESET",
}); });
factory FactoryResetModel.fromJson(Map<String, dynamic> json) { factory FactoryResetModel.fromJson(Map<String, dynamic> json) {
return FactoryResetModel( return FactoryResetModel(
devicesUuid: List<String>.from(json['devicesUuid']), devicesUuid: List<String>.from(json['devicesUuid']),
operationType: "RESET",
); );
} }

View File

@ -12,77 +12,95 @@ class DeviceSearchFilters extends StatefulWidget {
State<DeviceSearchFilters> createState() => _DeviceSearchFiltersState(); State<DeviceSearchFilters> createState() => _DeviceSearchFiltersState();
} }
class _DeviceSearchFiltersState extends State<DeviceSearchFilters> with HelperResponsiveLayout { class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
final TextEditingController communityController = TextEditingController(); with HelperResponsiveLayout {
final TextEditingController unitNameController = TextEditingController(); late final TextEditingController _unitNameController;
final TextEditingController productNameController = TextEditingController(); late final TextEditingController _productNameController;
@override
void initState() {
_unitNameController = TextEditingController();
_productNameController = TextEditingController();
super.initState();
}
@override
void dispose() {
_unitNameController.dispose();
_productNameController.dispose();
super.dispose();
}
List<Widget> get _widgets => [
_buildSearchField(
"Space Name",
_unitNameController,
200,
),
_buildSearchField(
"Device Name / Product Name",
_productNameController,
300,
),
_buildSearchResetButtons(),
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return isExtraLargeScreenSize(context) if (isExtraLargeScreenSize(context)) {
? Row( return Row(
children: [ children: _widgets.map(
_buildSearchField("Community", communityController, 200), (e) {
const SizedBox(width: 20), return Padding(
_buildSearchField("Space Name", unitNameController, 200), padding: const EdgeInsets.symmetric(vertical: 10),
const SizedBox(width: 20), child: e,
_buildSearchField("Device Name / Product Name", productNameController, 300), );
const SizedBox(width: 20), },
_buildSearchResetButtons(), ).toList(),
], );
) }
: Wrap(
spacing: 20, return Wrap(
runSpacing: 10, spacing: 20,
children: [ runSpacing: 10,
_buildSearchField( children: _widgets,
"Community", );
communityController,
200,
),
_buildSearchField("Space Name", unitNameController, 200),
_buildSearchField(
"Device Name / Product Name",
productNameController,
300,
),
_buildSearchResetButtons(),
],
);
} }
Widget _buildSearchField(String title, TextEditingController controller, double width) { Widget _buildSearchField(
return Container( String title,
child: StatefulTextField( TextEditingController controller,
title: title, double width,
width: width, ) {
elevation: 2, return StatefulTextField(
controller: controller, title: title,
onSubmitted: () { width: width,
context.read<DeviceManagementBloc>().add(SearchDevices( elevation: 2,
productName: productNameController.text, controller: controller,
unitName: unitNameController.text, onSubmitted: () {
community: communityController.text, final searchDevicesEvent = SearchDevices(
searchField: true)); productName: _productNameController.text,
}, unitName: _unitNameController.text,
onChanged: (p0) {}, searchField: true,
), );
context.read<DeviceManagementBloc>().add(searchDevicesEvent);
},
onChanged: (p0) {},
); );
} }
Widget _buildSearchResetButtons() { Widget _buildSearchResetButtons() {
return SearchResetButtons( return SearchResetButtons(
onSearch: () { onSearch: () => context.read<DeviceManagementBloc>().add(
context.read<DeviceManagementBloc>().add(SearchDevices( SearchDevices(
community: communityController.text, unitName: _unitNameController.text,
unitName: unitNameController.text, productName: _productNameController.text,
productName: productNameController.text, searchField: true,
searchField: true)); ),
}, ),
onReset: () { onReset: () {
communityController.clear(); _unitNameController.clear();
unitNameController.clear(); _productNameController.clear();
productNameController.clear();
context.read<DeviceManagementBloc>() context.read<DeviceManagementBloc>()
..add(ResetFilters()) ..add(ResetFilters())
..add(FetchDevices(context)); ..add(FetchDevices(context));

View File

@ -1,16 +1,16 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/style.dart';
class CommunityDropdown extends StatelessWidget { class CommunityDropdown extends StatelessWidget {
final String? selectedValue; final String? selectedValue;
final Function(String?) onChanged; final Function(String?) onChanged;
final TextEditingController _searchController = TextEditingController();
const CommunityDropdown({ CommunityDropdown({
Key? key, Key? key,
required this.selectedValue, required this.selectedValue,
required this.onChanged, required this.onChanged,
@ -34,58 +34,119 @@ class CommunityDropdown extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
BlocBuilder<SpaceTreeBloc, SpaceTreeState>( BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, state) { builder: (context, state) {
List<CommunityModel> communities =
state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList;
return SizedBox( return SizedBox(
child: DropdownButtonFormField<String>( child: Container(
dropdownColor: ColorsManager.whiteColors, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
underline: SizedBox(),
value: selectedValue, value: selectedValue,
items: communities.map((community) { items: state.communityList.map((community) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: community.uuid, value: community.uuid,
child: Text(' ${community.name}'), child: Text(
' ${community.name}',
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
); );
}).toList(), }).toList(),
onChanged: onChanged, onChanged: onChanged,
icon: const SizedBox.shrink(), style: TextStyle(color: Colors.black),
borderRadius: const BorderRadius.all(Radius.circular(10)),
hint: Padding( hint: Padding(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Text( child: Text(
"Please Select", " Please Select",
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
), ),
), ),
decoration: inputTextFormDeco().copyWith( customButton: Container(
contentPadding: EdgeInsets.zero, height: 45,
suffixIcon: Container( decoration: BoxDecoration(
padding: EdgeInsets.zero, border: Border.all(color: ColorsManager.textGray, width: 1.0),
width: 70, borderRadius: BorderRadius.circular(10),
height: 45, ),
decoration: BoxDecoration( child: Row(
color: Colors.grey[100], mainAxisAlignment: MainAxisAlignment.spaceBetween,
borderRadius: const BorderRadius.only( children: [
bottomRight: Radius.circular(10), Expanded(
topRight: Radius.circular(10), flex: 5,
child: Text(
selectedValue != null
? " ${state.communityList.firstWhere((element) => element.uuid == selectedValue).name}"
: ' Please Select',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color:
selectedValue != null ? Colors.black : ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
),
), ),
border: Border.all( Expanded(
color: ColorsManager.textGray, child: Container(
width: 1.0, decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
), ),
), ],
child: const Center( ),
child: Icon( ),
Icons.keyboard_arrow_down, dropdownStyleData: DropdownStyleData(
color: ColorsManager.textGray, maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
),
dropdownSearchData: DropdownSearchData(
searchController: _searchController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TextFormField(
style: const TextStyle(color: Colors.black),
controller: _searchController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 12,
),
hintText: 'Search for community...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
), ),
), ),
searchMatchFn: (item, searchValue) {
final communityName = (item.child as Text).data?.toLowerCase() ?? '';
return communityName.contains(searchValue.toLowerCase().trim());
},
),
onMenuStateChange: (isOpen) {
if (!isOpen) {
_searchController.clear();
}
},
menuItemStyleData: const MenuItemStyleData(
height: 40,
), ),
), ),
); ));
}, },
), ),
], ],

View File

@ -62,28 +62,34 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Divider(), const Divider(),
CommunityDropdown( Padding(
selectedValue: _selectedCommunity, padding: const EdgeInsets.only(left: 15, right: 15),
onChanged: (String? newValue) { child: CommunityDropdown(
setState(() { selectedValue: _selectedCommunity,
_selectedCommunity = newValue; onChanged: (String? newValue) {
_selectedSpace = null; setState(() {
}); _selectedCommunity = newValue;
if (newValue != null) { _selectedSpace = null;
_fetchSpaces(newValue); });
} if (newValue != null) {
}, _fetchSpaces(newValue);
}
},
),
), ),
const SizedBox(height: 16), const SizedBox(height: 5),
SpaceDropdown( Padding(
hintMessage: spaceHint, padding: const EdgeInsets.only(left: 15, right: 15),
spaces: spaces, child: SpaceDropdown(
selectedValue: _selectedSpace, hintMessage: spaceHint,
onChanged: (String? newValue) { spaces: spaces,
setState(() { selectedValue: _selectedSpace,
_selectedSpace = newValue; onChanged: (String? newValue) {
}); setState(() {
}, _selectedSpace = newValue;
});
},
),
), ),
const Divider(), const Divider(),
Row( Row(
@ -96,7 +102,6 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
), ),
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
@ -139,6 +144,7 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
), ),
], ],
), ),
SizedBox(height: 10),
], ],
), ),
); );

View File

@ -1,7 +1,8 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceDropdown extends StatelessWidget { class SpaceDropdown extends StatelessWidget {
final List<SpaceModel> spaces; final List<SpaceModel> spaces;
@ -33,62 +34,108 @@ class SpaceDropdown extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
DropdownButtonFormField<String>( SizedBox(
value: selectedValue, child: Container(
items: spaces.map((space) { decoration: BoxDecoration(
return DropdownMenuItem<String>( borderRadius: BorderRadius.circular(10),
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
' ${space.name}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
Text(
' ${space.lastThreeParents}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
),
),
],
));
}).toList(),
onChanged: onChanged,
icon: const SizedBox.shrink(),
borderRadius: const BorderRadius.all(Radius.circular(10)),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
), ),
), child: DropdownButton2<String>(
decoration: inputTextFormDeco().copyWith( underline: const SizedBox(),
contentPadding: EdgeInsets.zero, value: selectedValue,
suffixIcon: Container( items: spaces.map((space) {
width: 70, return DropdownMenuItem<String>(
height: 45, value: space.uuid,
decoration: BoxDecoration( child: Column(
color: Colors.grey[200], crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: const BorderRadius.only( mainAxisAlignment: MainAxisAlignment.start,
bottomRight: Radius.circular(10), children: [
topRight: Radius.circular(10), Text(
), ' ${space.name}',
border: Border.all( style:
color: ColorsManager.textGray, Theme.of(context).textTheme.bodyMedium!.copyWith(
width: 1.0, fontSize: 12,
color: ColorsManager.blackColor,
),
),
Text(
' ${space.lastThreeParents}',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
),
),
],
),
);
}).toList(),
onChanged: onChanged,
style: TextStyle(color: Colors.black),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
), ),
), ),
child: const Icon( customButton: Container(
Icons.keyboard_arrow_down, height: 45,
color: ColorsManager.textGray, decoration: BoxDecoration(
border:
Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
selectedValue != null
? spaces
.firstWhere((e) => e.uuid == selectedValue)
.name
: hintMessage,
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: selectedValue != null
? Colors.black
: ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 60,
), ),
), ),
), ),

View File

@ -33,8 +33,6 @@ class _RoutinesViewState extends State<RoutinesView> {
communityID: communityId, spaceID: spaceId)); communityID: communityId, spaceID: spaceId));
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
routineBloc.add(const CreateNewRoutineViewEvent(createRoutineView: true)); routineBloc.add(const CreateNewRoutineViewEvent(createRoutineView: true));
await Future.delayed(const Duration(milliseconds:500));
_bloc.add(const ResetSelectedEvent());
} }
@override @override

View File

@ -301,11 +301,18 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) { SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) {
setState(() { setState(() {
// Set the first space in the center or use passed position // Set the first space in the center or use passed position
Offset centerPosition = position ?? ConnectionHelper.getCenterPosition(screenSize); Offset newPosition;
if (parentIndex != null) {
newPosition =
getBalancedChildPosition(spaces[parentIndex]); // Ensure balanced position
} else {
newPosition = position ?? ConnectionHelper.getCenterPosition(screenSize);
}
SpaceModel newSpace = SpaceModel( SpaceModel newSpace = SpaceModel(
name: name, name: name,
icon: icon, icon: icon,
position: centerPosition, position: newPosition,
isPrivate: false, isPrivate: false,
children: [], children: [],
status: SpaceStatus.newSpace, status: SpaceStatus.newSpace,
@ -425,7 +432,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Connection( Connection(
startSpace: parent, startSpace: parent,
endSpace: child, endSpace: child,
direction: child.incomingConnection?.direction ?? "down", direction: "down",
), ),
); );
@ -522,6 +529,38 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
); );
} }
Offset getBalancedChildPosition(SpaceModel parent) {
int totalSiblings = parent.children.length + 1;
double totalWidth = (totalSiblings - 1) * 250; // Horizontal spacing
double startX = parent.position.dx - (totalWidth / 2);
Offset position = Offset(startX + (parent.children.length * 250), parent.position.dy + 180);
// Check for overlaps & adjust
while (spaces.any((s) => (s.position - position).distance < 250)) {
position = Offset(position.dx + 250, position.dy);
}
return position;
}
void realignTree() {
void updatePositions(SpaceModel node, double x, double y) {
node.position = Offset(x, y);
int numChildren = node.children.length;
double childStartX = x - ((numChildren - 1) * 250) / 2;
for (int i = 0; i < numChildren; i++) {
updatePositions(node.children[i], childStartX + (i * 250), y + 180);
}
}
if (spaces.isNotEmpty) {
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy);
}
}
void _onDuplicate(BuildContext parentContext) { void _onDuplicate(BuildContext parentContext) {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
@ -604,29 +643,57 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _duplicateSpace(SpaceModel space) { void _duplicateSpace(SpaceModel space) {
final Map<SpaceModel, SpaceModel> originalToDuplicate = {}; final Map<SpaceModel, SpaceModel> originalToDuplicate = {};
const double horizontalGap = 200.0; double horizontalGap = 250.0; // Increased spacing
const double verticalGap = 100.0; double verticalGap = 180.0; // Adjusted for better visualization
SpaceModel duplicateRecursive( print("🟢 Duplicating: ${space.name}");
SpaceModel original, Offset parentPosition, SpaceModel? duplicatedParent) {
Offset newPosition = Offset(parentPosition.dx + horizontalGap, original.position.dy);
while (spaces.any((s) => /// **Find a new position ensuring no overlap**
(s.position - newPosition).distance < horizontalGap && s.status != SpaceStatus.deleted)) { Offset getBalancedChildPosition(SpaceModel parent) {
newPosition += Offset(horizontalGap, 0); int totalSiblings = parent.children.length + 1;
double totalWidth = (totalSiblings - 1) * horizontalGap;
double startX = parent.position.dx - (totalWidth / 2);
Offset position = Offset(
startX + (parent.children.length * horizontalGap), parent.position.dy + verticalGap);
// **Check for overlaps & adjust**
while (spaces.any((s) => (s.position - position).distance < horizontalGap)) {
position = Offset(position.dx + horizontalGap, position.dy);
} }
print("🔹 New position for ${parent.name}: (${position.dx}, ${position.dy})");
return position;
}
/// **Realign the entire tree after duplication**
void realignTree() {
void updatePositions(SpaceModel node, double x, double y) {
node.position = Offset(x, y);
print("✅ Adjusted ${node.name} to (${x}, ${y})");
int numChildren = node.children.length;
double childStartX = x - ((numChildren - 1) * horizontalGap) / 2;
for (int i = 0; i < numChildren; i++) {
updatePositions(node.children[i], childStartX + (i * horizontalGap), y + verticalGap);
}
}
if (spaces.isNotEmpty) {
print("🔄 Realigning tree...");
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy);
}
}
/// **Recursive duplication logic**
SpaceModel duplicateRecursive(SpaceModel original, SpaceModel? duplicatedParent) {
Offset newPosition = duplicatedParent == null
? Offset(original.position.dx + horizontalGap, original.position.dy)
: getBalancedChildPosition(duplicatedParent);
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces); final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces);
print(
final List<SubspaceModel>? duplicatedSubspaces; "🟡 Duplicating ${original.name}${duplicatedName} at (${newPosition.dx}, ${newPosition.dy})");
final List<Tag>? duplicatedTags;
if (original.spaceModel != null) {
duplicatedTags = [];
duplicatedSubspaces = [];
} else {
duplicatedTags = original.tags;
duplicatedSubspaces = original.subspaces;
}
final duplicated = SpaceModel( final duplicated = SpaceModel(
name: duplicatedName, name: duplicatedName,
@ -637,12 +704,10 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
status: SpaceStatus.newSpace, status: SpaceStatus.newSpace,
parent: duplicatedParent, parent: duplicatedParent,
spaceModel: original.spaceModel, spaceModel: original.spaceModel,
subspaces: duplicatedSubspaces, subspaces: original.subspaces,
tags: duplicatedTags, tags: original.tags,
); );
originalToDuplicate[original] = duplicated;
setState(() { setState(() {
spaces.add(duplicated); spaces.add(duplicated);
_updateNodePosition(duplicated, duplicated.position); _updateNodePosition(duplicated, duplicated.position);
@ -651,60 +716,42 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection( final newConnection = Connection(
startSpace: duplicatedParent, startSpace: duplicatedParent,
endSpace: duplicated, endSpace: duplicated,
direction: original.incomingConnection?.direction ?? 'down', direction: "down",
); );
connections.add(newConnection); connections.add(newConnection);
duplicated.incomingConnection = newConnection; duplicated.incomingConnection = newConnection;
duplicatedParent.addOutgoingConnection(newConnection); duplicatedParent.addOutgoingConnection(newConnection);
duplicatedParent.children.add(duplicated);
print("🔗 Created connection: ${duplicatedParent.name}${duplicated.name}");
} }
if (original.parent != null && duplicatedParent == null) { // **Recalculate the whole tree to avoid overlaps**
final originalParent = original.parent!; realignTree();
final duplicatedParent = originalToDuplicate[originalParent] ?? originalParent;
final parentConnection = Connection(
startSpace: duplicatedParent,
endSpace: duplicated,
direction: original.incomingConnection?.direction ?? "down",
);
connections.add(parentConnection);
duplicated.incomingConnection = parentConnection;
duplicatedParent.addOutgoingConnection(parentConnection);
}
}); });
final childrenWithDownDirection = original.children // Recursively duplicate children
.where((child) => for (var child in original.children) {
child.incomingConnection?.direction == "down" && child.status != SpaceStatus.deleted) duplicateRecursive(child, duplicated);
.toList();
Offset childStartPosition = childrenWithDownDirection.length == 1
? duplicated.position
: newPosition + Offset(0, verticalGap);
for (final child in original.children) {
final isDownDirection = child.incomingConnection?.direction == "down" ?? false;
if (isDownDirection && childrenWithDownDirection.length == 1) {
childStartPosition = duplicated.position + Offset(0, verticalGap);
} else if (!isDownDirection) {
childStartPosition = duplicated.position + Offset(horizontalGap, 0);
}
final duplicatedChild = duplicateRecursive(child, childStartPosition, duplicated);
duplicated.children.add(duplicatedChild);
childStartPosition += Offset(0, verticalGap);
} }
return duplicated; return duplicated;
} }
/// **Handle root duplication**
if (space.parent == null) { if (space.parent == null) {
duplicateRecursive(space, space.position, null); print("🟠 Duplicating root node: ${space.name}");
SpaceModel duplicatedRoot = duplicateRecursive(space, null);
setState(() {
spaces.add(duplicatedRoot);
realignTree();
});
print("✅ Root duplication successful: ${duplicatedRoot.name}");
} else { } else {
final duplicatedParent = originalToDuplicate[space.parent!] ?? space.parent!; duplicateRecursive(space, space.parent);
duplicateRecursive(space, space.position, duplicatedParent);
} }
print("🟢 Finished duplication process for: ${space.name}");
} }
} }

View File

@ -47,18 +47,6 @@ class SpaceCardWidget extends StatelessWidget {
children: [ children: [
buildSpaceContainer(index), // Build the space container buildSpaceContainer(index), // Build the space container
if (isHovered) ...[ 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( PlusButtonWidget(
index: index, index: index,
direction: 'down', direction: 'down',

View File

@ -2,8 +2,7 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_m
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
class SpaceHelper { class SpaceHelper {
static SpaceModel? findSpaceByUuid( static SpaceModel? findSpaceByUuid(String? uuid, List<CommunityModel> communities) {
String? uuid, List<CommunityModel> communities) {
for (var community in communities) { for (var community in communities) {
for (var space in community.spaces) { for (var space in community.spaces) {
if (space.uuid == uuid) return space; if (space.uuid == uuid) return space;
@ -12,8 +11,7 @@ class SpaceHelper {
return null; return null;
} }
static SpaceModel? findSpaceByInternalId( static SpaceModel? findSpaceByInternalId(String? internalId, List<SpaceModel> spaces) {
String? internalId, List<SpaceModel> spaces) {
if (internalId != null) { if (internalId != null) {
for (var space in spaces) { for (var space in spaces) {
if (space.internalId == internalId) return space; if (space.internalId == internalId) return space;
@ -23,8 +21,7 @@ class SpaceHelper {
return null; return null;
} }
static String generateUniqueSpaceName( static String generateUniqueSpaceName(String originalName, List<SpaceModel> spaces) {
String originalName, List<SpaceModel> spaces) {
final baseName = originalName.replaceAll(RegExp(r'\(\d+\)$'), '').trim(); final baseName = originalName.replaceAll(RegExp(r'\(\d+\)$'), '').trim();
int maxNumber = 0; int maxNumber = 0;
@ -54,13 +51,10 @@ class SpaceHelper {
return space == selectedSpace || return space == selectedSpace ||
selectedSpace.parent?.internalId == space.internalId || selectedSpace.parent?.internalId == space.internalId ||
selectedSpace.children selectedSpace.children?.any((child) => child.internalId == space.internalId) == true;
?.any((child) => child.internalId == space.internalId) ==
true;
} }
static bool isNameConflict( static bool isNameConflict(String value, SpaceModel? parentSpace, SpaceModel? editSpace) {
String value, SpaceModel? parentSpace, SpaceModel? editSpace) {
final siblings = parentSpace?.children final siblings = parentSpace?.children
.where((child) => child.internalId != editSpace?.internalId) .where((child) => child.internalId != editSpace?.internalId)
.toList() ?? .toList() ??
@ -71,19 +65,17 @@ class SpaceHelper {
.toList() ?? .toList() ??
[]; [];
final editSiblingConflict = final editSiblingConflict = editSiblings.any((child) => child.name == value);
editSiblings.any((child) => child.name == value);
final siblingConflict = siblings.any((child) => child.name == value); final siblingConflict = siblings.any((child) => child.name == value);
final parentConflict = parentSpace?.name == value && final parentConflict =
parentSpace?.internalId != editSpace?.internalId; parentSpace?.name == value && parentSpace?.internalId != editSpace?.internalId;
final parentOfEditSpaceConflict = editSpace?.parent?.name == value && final parentOfEditSpaceConflict =
editSpace?.parent?.internalId != editSpace?.internalId; editSpace?.parent?.name == value && editSpace?.parent?.internalId != editSpace?.internalId;
final childConflict = final childConflict = editSpace?.children.any((child) => child.name == value) ?? false;
editSpace?.children.any((child) => child.name == value) ?? false;
return siblingConflict || return siblingConflict ||
parentConflict || parentConflict ||

View File

@ -194,9 +194,10 @@ class VisitorPasswordBloc
emit(DeviceLoaded()); emit(DeviceLoaded());
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
data = await AccessMangApi().fetchDevices(projectUuid); data = await AccessMangApi().fetchDoorLockDeviceList(projectUuid);
emit(TableLoaded(data)); emit(TableLoaded(data));
} catch (e) { } catch (e) {
print("error: $e");
emit(FailedState(e.toString())); emit(FailedState(e.toString()));
} }
} }

View File

@ -6,13 +6,23 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart'; import 'package:syncrow_web/utils/constants/api_const.dart';
class AccessMangApi { class AccessMangApi {
AccessMangApi() {
_validateEndpoints();
}
void _validateEndpoints() {
if (!ApiEndpoints.getDevices.contains('{projectId}')) {
throw Exception("Endpoint 'getDevices' must contain '{projectId}' placeholder.");
}
}
Future<List<PasswordModel>> fetchVisitorPassword(String projectId) async { Future<List<PasswordModel>> fetchVisitorPassword(String projectId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.visitorPassword.replaceAll('{projectId}', projectId), path: ApiEndpoints.visitorPassword,
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
List<dynamic> jsonData = json; List<dynamic> jsonData = json['data'] ?? [];
List<PasswordModel> passwordList = jsonData.map((jsonItem) { List<PasswordModel> passwordList = jsonData.map((jsonItem) {
return PasswordModel.fromJson(jsonItem); return PasswordModel.fromJson(jsonItem);
}).toList(); }).toList();
@ -25,17 +35,22 @@ class AccessMangApi {
} }
} }
Future fetchDevices(String projectId) async { Future fetchDoorLockDeviceList(String projectId) async {
try { try {
// The endpoint structure is already validated during initialization.
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getDevices.replaceAll('{projectId}', projectId), path: ApiEndpoints.getDevices.replaceAll('{projectId}', projectId),
queryParameters: {
'deviceType': 'DOOR_LOCK',
},
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
List<dynamic> jsonData = json; List<dynamic> jsonData = json['data'] ?? [];
List<DeviceModel> passwordList = jsonData.map((jsonItem) { List<DeviceModel> deviceList = jsonData.map((jsonItem) {
return DeviceModel.fromJson(jsonItem); return DeviceModel.fromJson(jsonItem);
}).toList(); }).toList();
return passwordList; return deviceList;
}, },
); );
return response; return response;
@ -52,14 +67,15 @@ class AccessMangApi {
String? invalidTime, String? invalidTime,
List<String>? devicesUuid}) async { List<String>? devicesUuid}) async {
final response = await HTTPService().post( final response = await HTTPService().post(
path: ApiEndpoints.sendOnlineOneTime, path: ApiEndpoints.visitorPassword,
body: jsonEncode({ body: jsonEncode({
"email": email, "email": email,
"passwordName": passwordName, "passwordName": passwordName,
"password": password, "password": password,
"devicesUuid": devicesUuid, "devicesUuid": devicesUuid,
"effectiveTime": effectiveTime, "effectiveTime": effectiveTime,
"invalidTime": invalidTime "invalidTime": invalidTime,
"operationType": "ONLINE_ONE_TIME",
}), }),
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
@ -84,13 +100,13 @@ class AccessMangApi {
"password": password, "password": password,
"effectiveTime": effectiveTime, "effectiveTime": effectiveTime,
"invalidTime": invalidTime, "invalidTime": invalidTime,
"operationType": "ONLINE_MULTIPLE_TIME",
}; };
if (scheduleList != null) { if (scheduleList != null) {
body["scheduleList"] = body["scheduleList"] = scheduleList.map((schedule) => schedule.toJson()).toList();
scheduleList.map((schedule) => schedule.toJson()).toList();
} }
final response = await HTTPService().post( final response = await HTTPService().post(
path: ApiEndpoints.sendOnlineMultipleTime, path: ApiEndpoints.visitorPassword,
body: jsonEncode(body), body: jsonEncode(body),
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
@ -105,8 +121,9 @@ class AccessMangApi {
Future postOffLineOneTime( Future postOffLineOneTime(
{String? email, String? passwordName, List<String>? devicesUuid}) async { {String? email, String? passwordName, List<String>? devicesUuid}) async {
final response = await HTTPService().post( final response = await HTTPService().post(
path: ApiEndpoints.sendOffLineOneTime, path: ApiEndpoints.visitorPassword,
body: jsonEncode({ body: jsonEncode({
"operationType": "OFFLINE_ONE_TIME",
"email": email, "email": email,
"passwordName": passwordName, "passwordName": passwordName,
"devicesUuid": devicesUuid "devicesUuid": devicesUuid
@ -126,13 +143,14 @@ class AccessMangApi {
String? invalidTime, String? invalidTime,
List<String>? devicesUuid}) async { List<String>? devicesUuid}) async {
final response = await HTTPService().post( final response = await HTTPService().post(
path: ApiEndpoints.sendOffLineMultipleTime, path: ApiEndpoints.visitorPassword,
body: jsonEncode({ body: jsonEncode({
"email": email, "email": email,
"devicesUuid": devicesUuid, "devicesUuid": devicesUuid,
"passwordName": passwordName, "passwordName": passwordName,
"effectiveTime": effectiveTime, "effectiveTime": effectiveTime,
"invalidTime": invalidTime, "invalidTime": invalidTime,
"operationType": "OFFLINE_MULTIPLE_TIME",
}), }),
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {

View File

@ -32,7 +32,7 @@ class DevicesManagementApi {
); );
return response; return response;
} catch (e) { } catch (e) {
debugPrint('fetchDevices Error fetching $e'); debugPrint('Error fetching device $e');
return []; return [];
} }
} }
@ -43,7 +43,7 @@ class DevicesManagementApi {
path: ApiEndpoints.getDeviceStatus.replaceAll('{uuid}', uuid), path: ApiEndpoints.getDeviceStatus.replaceAll('{uuid}', uuid),
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return DeviceStatus.fromJson(json); return DeviceStatus.fromJson(json['data']);
}, },
); );
return response; return response;
@ -60,7 +60,7 @@ class DevicesManagementApi {
Future getPowerClampInfo(String deviceId) async { Future getPowerClampInfo(String deviceId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.powerClamp.replaceAll('{powerClampUuid}', deviceId), path: ApiEndpoints.getDeviceStatus.replaceAll('{uuid}', deviceId),
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return json; return json;
@ -97,6 +97,7 @@ class DevicesManagementApi {
'devicesUuid': uuids, 'devicesUuid': uuids,
'code': code, 'code': code,
'value': value, 'value': value,
'operationType': 'COMMAND',
}; };
final response = await HTTPService().post( final response = await HTTPService().post(
@ -104,7 +105,7 @@ class DevicesManagementApi {
body: body, body: body,
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return (json['successResults'] as List).isNotEmpty; return json['success'] ?? false;
}, },
); );
@ -124,7 +125,7 @@ class DevicesManagementApi {
if (json == null || json.isEmpty || json == []) { if (json == null || json.isEmpty || json == []) {
return devices; return devices;
} }
for (var device in json['devices']) { for (var device in json['data']['devices']) {
devices.add(DeviceModel.fromJson(device)); devices.add(DeviceModel.fromJson(device));
} }
return devices; return devices;
@ -152,7 +153,7 @@ class DevicesManagementApi {
path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code), path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code),
showServerMessage: false, showServerMessage: false,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return DeviceReport.fromJson(json); return DeviceReport.fromJson(json['data']);
}, },
); );
return response; return response;
@ -168,7 +169,7 @@ class DevicesManagementApi {
.replaceAll('{endTime}', to ?? ''), .replaceAll('{endTime}', to ?? ''),
showServerMessage: false, showServerMessage: false,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return DeviceReport.fromJson(json); return DeviceReport.fromJson(json['data']);
}, },
); );
return response; return response;
@ -184,7 +185,7 @@ class DevicesManagementApi {
queryParameters: queryParameters, queryParameters: queryParameters,
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return DeviceStatus.fromJson(json['status']); return DeviceStatus.fromJson(json['data']['status']);
}, },
); );
return response; return response;

View File

@ -9,21 +9,8 @@ abstract class ApiEndpoints {
static const String sendOtp = '/authentication/user/send-otp'; static const String sendOtp = '/authentication/user/send-otp';
static const String verifyOtp = '/authentication/user/verify-otp'; static const String verifyOtp = '/authentication/user/verify-otp';
static const String getRegion = '/region'; static const String getRegion = '/region';
static const String visitorPassword = static const String visitorPassword = '/visitor-passwords';
'/projects/{projectId}/visitor-password'; static const String getDevices = '/projects/{projectId}/devices';
static const String getDevices =
'/projects/{projectId}/visitor-password/devices';
static const String sendOnlineOneTime =
'/visitor-password/temporary-password/online/one-time';
static const String sendOnlineMultipleTime =
'/visitor-password/temporary-password/online/multiple-time';
//offline Password
static const String sendOffLineOneTime =
'/visitor-password/temporary-password/offline/one-time';
static const String sendOffLineMultipleTime =
'/visitor-password/temporary-password/offline/multiple-time';
static const String getUser = '/user/{userUuid}'; static const String getUser = '/user/{userUuid}';
@ -32,15 +19,15 @@ abstract class ApiEndpoints {
static const String getAllDevices = '/projects/{projectId}/devices'; static const String getAllDevices = '/projects/{projectId}/devices';
static const String getSpaceDevices = static const String getSpaceDevices =
'/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices'; '/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices';
static const String getDeviceStatus = '/device/{uuid}/functions/status'; static const String getDeviceStatus = '/devices/{uuid}/functions/status';
static const String getBatchStatus = '/device/status/batch'; static const String getBatchStatus = '/devices/batch';
static const String deviceControl = '/device/{uuid}/control'; static const String deviceControl = '/devices/{uuid}/command';
static const String deviceBatchControl = '/device/control/batch'; static const String deviceBatchControl = '/devices/batch';
static const String gatewayApi = '/device/gateway/{gatewayUuid}/devices'; static const String gatewayApi = '/devices/gateway/{gatewayUuid}/devices';
static const String openDoorLock = '/door-lock/open/{doorLockUuid}'; static const String openDoorLock = '/door-lock/open/{doorLockUuid}';
static const String getDeviceLogs = '/device/report-logs/{uuid}?code={code}'; static const String getDeviceLogs = '/devices/{uuid}/report-logs?code={code}';
// Space Module // Space Module
static const String createSpace = static const String createSpace =
@ -70,18 +57,13 @@ abstract class ApiEndpoints {
static const String createUserCommunity = static const String createUserCommunity =
'/projects/{projectId}/communities/user'; '/projects/{projectId}/communities/user';
static const String getDeviceLogsByDate = static const String getDeviceLogsByDate =
'/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; '/devices/{uuid}/report-logs?code={code}&startTime={startTime}&endTime={endTime}';
static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}';
static const String getScheduleByDeviceId = static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}';
'/schedule/{deviceUuid}?category={category}'; static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}';
static const String deleteScheduleByDeviceId = static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}';
'/schedule/{deviceUuid}/{scheduleUuid}'; static const String factoryReset = '/devices/batch';
static const String updateScheduleByDeviceId =
'/schedule/enable/{deviceUuid}';
static const String factoryReset = '/device/factory/reset/{deviceUuid}';
static const String powerClamp =
'/device/{powerClampUuid}/power-clamp/status';
//product //product
static const String listProducts = '/products'; static const String listProducts = '/products';