Compare commits

..

19 Commits

Author SHA1 Message Date
ab23be9828 fixed the issue in selecting space model and tag 2025-04-28 14:36:27 +04:00
687b68ab22 Merge pull request #163 from SyncrowIOT/fix/duplication-flatten
Fix/duplication-flatten
2025-04-28 13:00:17 +04:00
25614c3dd0 fix the flatten 2025-04-28 12:59:16 +04:00
7cbe20ae88 remove unused code 2025-04-28 12:56:29 +04:00
349fe6c555 realignment 2025-04-28 11:58:41 +04:00
9779f3783c Merge branch 'dev' of https://github.com/SyncrowIOT/web into dev 2025-04-28 10:03:31 +04:00
fe3db663b6 realign initially 2025-04-28 10:03:27 +04:00
888d444752 Merge pull request #160 from SyncrowIOT/SP-1478-FE-On-devices-management-page-when-we-open-power-clamp-device-loading-indicator-remains-loading-and-no-data-is-displayed
Sp 1478 fe on devices management page when we open power clamp device loading indicator remains loading and no data is displayed
2025-04-28 08:59:17 +03:00
bab5e06968 Merge pull request #159 from SyncrowIOT/SP-1463-rework
Sp 1463 rework
2025-04-28 08:58:39 +03:00
d7b6174dee Merge pull request #162 from SyncrowIOT:bugfix/save-spaces
fixed the save issue
2025-04-28 00:37:33 +04:00
6ef0b2f9d1 fixed the save issue 2025-04-28 00:36:58 +04:00
3ceb03826e Merge pull request #161 from SyncrowIOT/bugfix/searchquery 2025-04-27 22:44:57 +04:00
52608b1f35 fixed search query 2025-04-27 22:42:57 +04:00
ac2996629e resolved an exception that was thrown when resizing the DeviceSearchFilters. 2025-04-27 15:42:50 +03:00
51c52c66cb SP-1478-FE-On-devices-management-page-when-we-open-power-clamp-device-loading-indicator-remains-loading-and-no-data-is-displayed 2025-04-27 15:18:19 +03:00
0f56273d99 SP-1408 2025-04-27 12:10:38 +03:00
f30d7c0117 Merge pull request #158 from SyncrowIOT:bugfix/duplicate-space
Bugfix/duplicate-space
2025-04-27 11:13:07 +04:00
976d6e385a duplicated space 2025-04-27 11:12:03 +04:00
ff07e7509d fixed the issue in aligning child space 2025-04-26 16:04:41 +04:00
12 changed files with 315 additions and 235 deletions

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SearchResetButtons extends StatelessWidget {
const SearchResetButtons({
@ -17,8 +17,10 @@ class SearchResetButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 25),

View File

@ -108,7 +108,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
return DeviceManagementBody(
devices: deviceState.filteredDevices);
} else {
return const Center(child: Text('Error fetching Devices'));
return const DeviceManagementBody(devices: []);
}
},
);

View File

@ -72,6 +72,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
child: state is DeviceManagementLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: isLargeScreenSize(context)

View File

@ -14,29 +14,29 @@ class DeviceSearchFilters extends StatefulWidget {
class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
with HelperResponsiveLayout {
final _unitNameController = TextEditingController();
final _productNameController = TextEditingController();
late final TextEditingController _unitNameController;
late final TextEditingController _productNameController;
List<Widget> get _widgets => [
_buildSearchField("Space Name", _unitNameController, 200),
_buildSearchField("Device Name / Product Name", _productNameController, 300),
_buildSearchResetButtons(),
];
@override
void initState() {
_unitNameController = TextEditingController();
_productNameController = TextEditingController();
super.initState();
}
@override
Widget build(BuildContext context) {
if (isExtraLargeScreenSize(context)) {
return Row(
children: _widgets
.map((e) => Padding(padding: const EdgeInsets.all(10), child: e))
.toList(),
);
}
return Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 20,
runSpacing: 10,
children: _widgets,
children: [
_buildSearchField("Space Name", _unitNameController, 200),
_buildSearchField("Device Name / Product Name", _productNameController, 300),
_buildSearchResetButtons(),
],
);
}

View File

@ -217,29 +217,31 @@ class SmartPowerBloc extends Bloc<SmartPowerEvent, SmartPowerState> {
try {
var status =
await DevicesManagementApi().getPowerClampInfo(event.deviceId);
deviceStatus = PowerClampModel.fromJson(status);
deviceStatus = PowerClampModel.fromJson(status as Map<String, Object?>? ??{});
final phaseADataPoints = deviceStatus.status.phaseA.dataPoints;
final phaseBDataPoints = deviceStatus.status.phaseB.dataPoints;
final phaseCDataPoints = deviceStatus.status.phaseC.dataPoints;
phaseData = [
{
'name': 'Phase A',
'voltage': '${deviceStatus.status.phaseA.dataPoints[0].value / 10} V',
'current': '${deviceStatus.status.phaseA.dataPoints[1].value / 10} A',
'activePower': '${deviceStatus.status.phaseA.dataPoints[2].value} W',
'powerFactor': '${deviceStatus.status.phaseA.dataPoints[3].value}',
'voltage': '${(phaseADataPoints.elementAtOrNull(0)?.value as num? ?? 0) / 10} V',
'current': '${(phaseADataPoints.elementAtOrNull(1)?.value as num? ?? 0) / 10} A',
'activePower': '${phaseADataPoints.elementAtOrNull(2)?.value??'N/A'} W',
'powerFactor': '${phaseADataPoints.elementAtOrNull(3)?.value??'N/A'}',
},
{
'name': 'Phase B',
'voltage': '${deviceStatus.status.phaseB.dataPoints[0].value / 10} V',
'current': '${deviceStatus.status.phaseB.dataPoints[1].value / 10} A',
'activePower': '${deviceStatus.status.phaseB.dataPoints[2].value} W',
'powerFactor': '${deviceStatus.status.phaseB.dataPoints[3].value}',
'voltage': '${(phaseBDataPoints .elementAtOrNull(0)?.value as num? ?? 0) / 10} V',
'current': '${(phaseBDataPoints .elementAtOrNull(1)?.value as num? ?? 0) / 10} A',
'activePower': '${phaseBDataPoints.elementAtOrNull(2)?.value??'N/A'} W',
'powerFactor': '${phaseBDataPoints.elementAtOrNull(3)?.value??'N/A'}',
},
{
'name': 'Phase C',
'voltage': '${deviceStatus.status.phaseC.dataPoints[0].value / 10} V',
'current': '${deviceStatus.status.phaseC.dataPoints[1].value / 10} A',
'activePower': '${deviceStatus.status.phaseC.dataPoints[2].value} W',
'powerFactor': '${deviceStatus.status.phaseC.dataPoints[3].value}',
'voltage': '${(phaseCDataPoints.elementAtOrNull(0)?.value as num? ?? 0) / 10} V',
'current': '${(phaseCDataPoints.elementAtOrNull(1)?.value as num? ?? 0) / 10} A',
'activePower': '${phaseCDataPoints.elementAtOrNull(2)?.value ?? 'N/A'} W',
'powerFactor': '${phaseCDataPoints.elementAtOrNull(3)?.value ?? 'N/A'}',
},
];
emit(GetDeviceStatus());
@ -785,7 +787,7 @@ class SmartPowerBloc extends Bloc<SmartPowerEvent, SmartPowerState> {
void selectDateRange() async {
DateTime startDate = dateTime!;
DateTime endDate = DateTime(startDate.year, startDate.month + 1, 1)
.subtract(Duration(days: 1));
.subtract(const Duration(days: 1));
String formattedEndDate = DateFormat('dd/MM/yyyy').format(endDate);
endChartDate = ' - $formattedEndDate';
}

View File

@ -12,9 +12,9 @@ class PowerClampModel {
factory PowerClampModel.fromJson(Map<String, dynamic> json) {
return PowerClampModel(
productUuid: json['productUuid'],
productType: json['productType'],
status: PowerStatus.fromJson(json['status']),
productUuid: json['productUuid'] as String? ?? '',
productType: json['productType'] as String? ?? '',
status: PowerStatus.fromJson(json['status'] as Map<String, dynamic>? ?? {}),
);
}
@ -26,7 +26,7 @@ class PowerClampModel {
return PowerClampModel(
productUuid: productUuid ?? this.productUuid,
productType: productType ?? this.productType,
status: statusPower ?? this.status,
status: statusPower ?? status,
);
}
}
@ -46,12 +46,10 @@ class PowerStatus {
factory PowerStatus.fromJson(Map<String, dynamic> json) {
return PowerStatus(
phaseA: Phase.fromJson(json['phaseA']),
phaseB: Phase.fromJson(json['phaseB']),
phaseC: Phase.fromJson(json['phaseC']),
general: Phase.fromJson(json['general']
// List<DataPoint>.from(
// json['general'].map((x) => DataPoint.fromJson(x))),
phaseA: Phase.fromJson(json['phaseA']as List<dynamic>? ?? []),
phaseB: Phase.fromJson(json['phaseB']as List<dynamic>? ?? []),
phaseC: Phase.fromJson(json['phaseC']as List<dynamic>? ?? []),
general: Phase.fromJson(json['general']as List<dynamic>? ?? []
));
}
}
@ -69,30 +67,30 @@ class Phase {
}
class DataPoint {
dynamic code;
dynamic customName;
dynamic dpId;
dynamic time;
dynamic type;
dynamic value;
final String? code;
final String? customName;
final int? dpId;
final int? time;
final String? type;
final dynamic value;
DataPoint({
required this.code,
required this.customName,
required this.dpId,
required this.time,
required this.type,
required this.value,
this.code,
this.customName,
this.dpId,
this.time,
this.type,
this.value,
});
factory DataPoint.fromJson(Map<String, dynamic> json) {
return DataPoint(
code: json['code'],
customName: json['customName'],
dpId: json['dpId'],
time: json['time'],
type: json['type'],
value: json['value'],
code: json['code'] as String?,
customName: json['customName'] as String?,
dpId: json['dpId'] as int?,
time: json['time'] as int?,
type: json['type'] as String?,
value: json['value'] as dynamic,
);
}
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EnergyConsumptionPage extends StatefulWidget {
@ -10,7 +10,8 @@ class EnergyConsumptionPage extends StatefulWidget {
final Widget widget;
final Function()? onTap;
EnergyConsumptionPage({
const EnergyConsumptionPage({
super.key,
required this.chartData,
required this.totalConsumption,
required this.date,
@ -91,11 +92,12 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 10),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.11,
height: MediaQuery.sizeOf(context).height * 0.09,
child: LineChart(
LineChartData(
lineTouchData: LineTouchData(
@ -151,7 +153,7 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
child: RotatedBox(
quarterTurns: -1,
child: Text(_chartData[index].time,
style: TextStyle(fontSize: 10)),
style: const TextStyle(fontSize: 10)),
),
);
}
@ -190,8 +192,8 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
spots: _chartData
.asMap()
.entries
.map((entry) => FlSpot(entry.key.toDouble(),
entry.value.consumption))
.map((entry) => FlSpot(
entry.key.toDouble(), entry.value.consumption))
.toList(),
isCurved: true,
color: ColorsManager.primaryColor.withOpacity(0.6),
@ -218,7 +220,7 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
borderData: FlBorderData(
show: false,
border: Border.all(
color: Color(0xff023DFE).withOpacity(0.7),
color: const Color(0xff023DFE).withOpacity(0.7),
width: 10,
),
),
@ -253,11 +255,9 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
child: InkWell(
onTap: widget.onTap,
child: Center(
child: SizedBox(
child: Padding(
padding: const EdgeInsets.all(5),
child: Text(widget.date),
),
child: Padding(
padding: const EdgeInsets.all(5),
child: Text(widget.date),
),
),
),

View File

@ -12,8 +12,7 @@ import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
//Smart Power Clamp
class SmartPowerDeviceControl extends StatelessWidget
with HelperResponsiveLayout {
class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout {
final String deviceId;
const SmartPowerDeviceControl({super.key, required this.deviceId});
@ -25,27 +24,27 @@ class SmartPowerDeviceControl extends StatelessWidget
..add(SmartPowerFetchDeviceEvent(deviceId)),
child: BlocBuilder<SmartPowerBloc, SmartPowerState>(
builder: (context, state) {
final _blocProvider = BlocProvider.of<SmartPowerBloc>(context);
final blocProvider = BlocProvider.of<SmartPowerBloc>(context);
if (state is SmartPowerLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is FakeState) {
return _buildStatusControls(
currentPage: _blocProvider.currentPage,
currentPage: blocProvider.currentPage,
context: context,
blocProvider: _blocProvider,
blocProvider: blocProvider,
);
} else if (state is GetDeviceStatus) {
return _buildStatusControls(
currentPage: _blocProvider.currentPage,
currentPage: blocProvider.currentPage,
context: context,
blocProvider: _blocProvider,
blocProvider: blocProvider,
);
} else if (state is FilterRecordsState) {
return _buildStatusControls(
currentPage: _blocProvider.currentPage,
currentPage: blocProvider.currentPage,
context: context,
blocProvider: _blocProvider,
blocProvider: blocProvider,
);
}
return const Center(child: CircularProgressIndicator());
@ -60,7 +59,7 @@ class SmartPowerDeviceControl extends StatelessWidget
required SmartPowerBloc blocProvider,
required int currentPage,
}) {
PageController _pageController = PageController(initialPage: currentPage);
PageController pageController = PageController(initialPage: currentPage);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 50),
child: DeviceControlsContainer(
@ -85,25 +84,31 @@ class SmartPowerDeviceControl extends StatelessWidget
PowerClampInfoCard(
iconPath: Assets.powerActiveIcon,
title: 'Active',
value: blocProvider
.deviceStatus.status.general.dataPoints[2].value
.toString(),
value: blocProvider.deviceStatus.status.general.dataPoints
.elementAtOrNull(2)
?.value
.toString() ??
'',
unit: '',
),
PowerClampInfoCard(
iconPath: Assets.voltMeterIcon,
title: 'Current',
value: blocProvider
.deviceStatus.status.general.dataPoints[1].value
.toString(),
value: blocProvider.deviceStatus.status.general.dataPoints
.elementAtOrNull(1)
?.value
.toString() ??
'',
unit: ' A',
),
PowerClampInfoCard(
iconPath: Assets.frequencyIcon,
title: 'Frequency',
value: blocProvider
.deviceStatus.status.general.dataPoints[4].value
.toString(),
value: blocProvider.deviceStatus.status.general.dataPoints
.elementAtOrNull(4)
?.value
.toString() ??
'',
unit: ' Hz',
),
],
@ -142,7 +147,7 @@ class SmartPowerDeviceControl extends StatelessWidget
icon: const Icon(Icons.arrow_left),
onPressed: () {
blocProvider.add(SmartPowerArrowPressedEvent(-1));
_pageController.previousPage(
pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
@ -162,7 +167,7 @@ class SmartPowerDeviceControl extends StatelessWidget
icon: const Icon(Icons.arrow_right),
onPressed: () {
blocProvider.add(SmartPowerArrowPressedEvent(1));
_pageController.nextPage(
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
@ -177,7 +182,7 @@ class SmartPowerDeviceControl extends StatelessWidget
Expanded(
flex: 2,
child: PageView(
controller: _pageController,
controller: pageController,
onPageChanged: (int page) {
blocProvider.add(SmartPowerPageChangedEvent(page));
},
@ -190,8 +195,8 @@ class SmartPowerDeviceControl extends StatelessWidget
blocProvider.add(SelectDateEvent(context: context));
blocProvider.add(FilterRecordsByDateEvent(
selectedDate: blocProvider.dateTime!,
viewType: blocProvider
.views[blocProvider.currentIndex]));
viewType:
blocProvider.views[blocProvider.currentIndex]));
},
widget: blocProvider.dateSwitcher(),
chartData: blocProvider.energyDataList.isNotEmpty

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.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/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
@ -246,7 +247,9 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
final previousState = state;
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = event.context.read<SpaceTreeBloc>();
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc);
var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
await fetchSpaceModels();
await fetchTags();
@ -277,11 +280,13 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
LoadCommunityAndSpacesEvent event,
Emitter<SpaceManagementState> emit,
) async {
var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
var spaceBloc = event.context.read<SpaceTreeBloc>();
_onloadProducts();
await fetchTags();
// Wait until `communityList` is loaded
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc);
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
// Fetch space models after communities are available
final prevSpaceModels = await fetchSpaceModels();
@ -292,23 +297,38 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
allTags: _cachedTags ?? []));
}
Future<List<CommunityModel>> _waitForCommunityList(SpaceTreeBloc spaceBloc) async {
Future<List<CommunityModel>> _waitForCommunityList(
SpaceTreeBloc spaceBloc, SpaceTreeState spaceTreeState) async {
// Check if communityList is already populated
if (spaceBloc.state.communityList.isNotEmpty) {
return spaceBloc.state.communityList;
final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
if (filteredCommunities.isNotEmpty) {
return filteredCommunities;
}
final completer = Completer<List<CommunityModel>>();
final subscription = spaceBloc.stream.listen((state) {
if (state.communityList.isNotEmpty) {
completer.complete(state.communityList);
if (!completer.isCompleted && state.communityList.isNotEmpty) {
completer
.complete(state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList);
}
});
try {
final communities = await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
if (!completer.isCompleted) {
completer.complete([]);
}
return [];
},
);
// Return the list once available, then cancel the listener
final communities = await completer.future;
await subscription.cancel();
return communities;
return communities;
} finally {
await subscription.cancel();
}
}
void _onCommunityDelete(
@ -491,12 +511,21 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
Future<List<SpaceModel>> saveSpacesHierarchically(
BuildContext context, List<SpaceModel> spaces, String communityUuid) async {
final orderedSpaces = flattenHierarchy(spaces);
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = context.read<SpaceTreeBloc>();
List<CommunityModel> communities = spaceBloc.state.communityList;
CommunityModel? selectedCommunity = communities.firstWhere(
(community) => community.uuid == communityUuid,
);
CommunityModel? selectedCommunity;
try {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
selectedCommunity = filteredCommunities.firstWhere(
(community) => community.uuid == communityUuid,
);
} catch (e) {
return [];
}
final parentsToDelete = orderedSpaces.where((space) =>
space.status == SpaceStatus.deleted &&
@ -605,11 +634,11 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
projectId: projectUuid);
} else {
// Call create if the space does not have a UUID
final List<CreateTagBodyModel> tagBodyModels = space.tags != null
List<CreateTagBodyModel> tagBodyModels = space.tags != null
? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList()
: [];
final createSubspaceBodyModels = space.subspaces?.map((subspace) {
var createSubspaceBodyModels = space.subspaces?.map((subspace) {
final tagBodyModels =
subspace.tags?.map((tag) => tag.toCreateTagBodyModel()).toList() ?? [];
return CreateSubspaceModel()
@ -618,6 +647,11 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
}).toList() ??
[];
if (space.spaceModel?.uuid != null) {
tagBodyModels = [];
createSubspaceBodyModels = [];
}
final response = await _api.createSpace(
communityId: communityUuid,
name: space.name,
@ -669,9 +703,12 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
try {
await fetchTags();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = event.context.read<SpaceTreeBloc>();
List<CommunityModel> communities = spaceBloc.state.communityList;
final spaceTreeState = event.context.read<SpaceTreeBloc>().state;
final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
List<CommunityModel> communities = filteredCommunities;
var prevSpaceModels = await fetchSpaceModels();

View File

@ -1,4 +1,6 @@
// Flutter imports
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -70,6 +72,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '',
);
realignTree();
_transformationController = TransformationController();
if (widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -94,6 +98,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
realignTree();
});
}
@ -336,6 +341,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
}
spaces.add(newSpace);
_updateNodePosition(newSpace, newSpace.position);
realignTree();
});
},
);
@ -401,21 +407,31 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
List<SpaceModel> flattenSpaces(List<SpaceModel> spaces) {
List<SpaceModel> result = [];
Map<String, SpaceModel> idToSpace = {};
void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) {
return;
}
result.add(space);
idToSpace[space.internalId] = space;
for (var child in space.children) {
flatten(child);
}
}
for (var space in spaces) {
flatten(space);
}
for (var space in result) {
if (space.parent != null) {
space.parent = idToSpace[space.parent!.internalId];
}
}
return result;
}
@ -450,7 +466,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _saveSpaces() {
if (widget.selectedCommunity == null) {
debugPrint("No community selected for saving spaces.");
return;
}
@ -465,7 +480,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
}
String communityUuid = widget.selectedCommunity!.uuid;
context.read<SpaceManagementBloc>().add(SaveSpacesEvent(
context,
spaces: spacesToSave,
@ -530,35 +544,83 @@ 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);
const double nodeWidth = 200;
const double verticalGap = 180;
Offset position = Offset(startX + (parent.children.length * 250), parent.position.dy + 180);
if (parent.children.isEmpty) {
// First child → exactly center
return Offset(parent.position.dx, parent.position.dy + verticalGap);
} else {
// More children → arrange them spaced horizontally
double totalWidth = (parent.children.length) * (nodeWidth + 60);
double startX = parent.position.dx - (totalWidth / 2);
// Check for overlaps & adjust
while (spaces.any((s) => (s.position - position).distance < 250)) {
position = Offset(position.dx + 250, position.dy);
double childX = startX + (parent.children.length * (nodeWidth + 60));
return Offset(childX, parent.position.dy + verticalGap);
}
return position;
}
void realignTree() {
void updatePositions(SpaceModel node, double x, double y) {
node.position = Offset(x, y);
const double nodeWidth = 200;
const double nodeHeight = 100;
const double horizontalGap = 60;
const double verticalGap = 180;
const double rootGap = 400; // extra space between different roots
int numChildren = node.children.length;
double childStartX = x - ((numChildren - 1) * 250) / 2;
double canvasRightEdge = 1000;
double canvasBottomEdge = 1000;
for (int i = 0; i < numChildren; i++) {
updatePositions(node.children[i], childStartX + (i * 250), y + 180);
double calculateSubtreeWidth(SpaceModel node) {
if (node.children.isEmpty) return nodeWidth;
double totalWidth = 0;
for (var child in node.children) {
totalWidth += calculateSubtreeWidth(child) + horizontalGap;
}
return totalWidth - horizontalGap;
}
void layoutSubtree(SpaceModel node, double startX, double y) {
double subtreeWidth = calculateSubtreeWidth(node);
double centerX = startX + subtreeWidth / 2 - nodeWidth / 2;
node.position = Offset(centerX, y);
canvasRightEdge = max(canvasRightEdge, centerX + nodeWidth);
canvasBottomEdge = max(canvasBottomEdge, y + nodeHeight);
if (node.children.length == 1) {
final child = node.children.first;
layoutSubtree(child, centerX, y + verticalGap);
} else {
double childX = startX;
for (var child in node.children) {
double childWidth = calculateSubtreeWidth(child);
layoutSubtree(child, childX, y + verticalGap);
childX += childWidth + horizontalGap;
}
}
}
if (spaces.isNotEmpty) {
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy);
// ⚡ New: layout each root separately
final List<SpaceModel> roots = spaces
.where((s) =>
s.parent == null &&
s.status != SpaceStatus.deleted &&
s.status != SpaceStatus.parentDeleted)
.toList();
double currentX = 100; // start some margin from left
double currentY = 100; // top margin
for (var root in roots) {
layoutSubtree(root, currentX, currentY);
double rootWidth = calculateSubtreeWidth(root);
currentX += rootWidth + rootGap;
}
setState(() {
canvasWidth = canvasRightEdge + 400;
canvasHeight = canvasBottomEdge + 400;
});
}
void _onDuplicate(BuildContext parentContext) {
@ -642,63 +704,19 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
}
void _duplicateSpace(SpaceModel space) {
final Map<SpaceModel, SpaceModel> originalToDuplicate = {};
double horizontalGap = 250.0; // Increased spacing
double verticalGap = 180.0; // Adjusted for better visualization
final double horizontalGap = 250.0;
final double verticalGap = 180.0;
final double nodeWidth = 200;
final double nodeHeight = 100;
final double breathingSpace = 300.0;
print("🟢 Duplicating: ${space.name}");
/// **Find a new position ensuring no overlap**
Offset getBalancedChildPosition(SpaceModel parent) {
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**
/// Helper to recursively duplicate a node and its children
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);
print(
"🟡 Duplicating ${original.name}${duplicatedName} at (${newPosition.dx}, ${newPosition.dy})");
final duplicated = SpaceModel(
name: duplicatedName,
icon: original.icon,
position: newPosition,
position: original.position,
isPrivate: original.isPrivate,
children: [],
status: SpaceStatus.newSpace,
@ -708,28 +726,22 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
tags: original.tags,
);
setState(() {
spaces.add(duplicated);
_updateNodePosition(duplicated, duplicated.position);
spaces.add(duplicated);
if (duplicatedParent != null) {
final newConnection = Connection(
startSpace: duplicatedParent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
duplicatedParent.addOutgoingConnection(newConnection);
duplicatedParent.children.add(duplicated);
print("🔗 Created connection: ${duplicatedParent.name}${duplicated.name}");
}
if (duplicatedParent != null) {
final newConnection = Connection(
startSpace: duplicatedParent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
// **Recalculate the whole tree to avoid overlaps**
realignTree();
});
duplicated.incomingConnection = newConnection;
duplicatedParent.addOutgoingConnection(newConnection);
duplicatedParent.children.add(duplicated);
}
// Recursively duplicate children
// Duplicate its children recursively
for (var child in original.children) {
duplicateRecursive(child, duplicated);
}
@ -737,21 +749,30 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return duplicated;
}
/// **Handle root duplication**
if (space.parent == null) {
print("🟠 Duplicating root node: ${space.name}");
SpaceModel duplicatedRoot = duplicateRecursive(space, null);
setState(() {
spaces.add(duplicatedRoot);
/// Actual duplication process
setState(() {
if (space.parent == null) {
// Duplicating a ROOT node
duplicateRecursive(space, null);
connections = createConnections(spaces);
realignTree();
});
} else {
final parent = space.parent!;
print("✅ Root duplication successful: ${duplicatedRoot.name}");
} else {
duplicateRecursive(space, space.parent);
}
print("🟢 Finished duplication process for: ${space.name}");
final duplicated = duplicateRecursive(space, parent);
final newConnection = Connection(
startSpace: parent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
parent.addOutgoingConnection(newConnection);
parent.children.add(duplicated);
connections = createConnections(spaces);
realignTree();
//_realignSubtree(space.parent!);
}
});
}
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/empty_search_result_widget.dart';
@ -36,6 +38,7 @@ class SidebarWidget extends StatefulWidget {
class _SidebarWidgetState extends State<SidebarWidget> {
late final ScrollController _scrollController;
Timer? _debounce;
String _searchQuery = '';
String? _selectedSpaceUuid;
@ -43,10 +46,10 @@ class _SidebarWidgetState extends State<SidebarWidget> {
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
_selectedId = widget.selectedSpaceUuid;
super.initState();
}
void _onScroll() {
@ -67,11 +70,22 @@ class _SidebarWidgetState extends State<SidebarWidget> {
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
_debounce?.cancel();
super.dispose();
}
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
setState(() {
_searchQuery = query;
});
context.read<SpaceTreeBloc>().add(SearchQueryEvent(query));
});
}
@override
void didUpdateWidget(covariant SidebarWidget oldWidget) {
if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) {
@ -91,7 +105,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
final filteredCommunities = spaceTreeState.isSearching
final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
@ -104,13 +118,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
children: [
SidebarHeader(onAddCommunity: _onAddCommunity),
CustomSearchBar(
onSearchChanged: (query) {
setState(() {
_searchQuery = query;
});
context.read<SpaceTreeBloc>().add(SearchQueryEvent(query));
},
onSearchChanged: _onSearchChanged,
),
const SizedBox(height: 16),
Expanded(
@ -123,10 +131,15 @@ class _SidebarWidgetState extends State<SidebarWidget> {
communities: filteredCommunities,
itemBuilder: (context, index) {
if (index == filteredCommunities.length) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.paginationIsLoading) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),
);
} else {
return const SizedBox.shrink();
}
}
return _buildCommunityTile(context, filteredCommunities[index]);
}),

View File

@ -281,6 +281,7 @@ class CommunitySpaceManagementApi {
return json['success'] ?? false;
},
);
return response;
} catch (e) {
debugPrint('Error updating space: $e');