Compare commits

..

43 Commits

Author SHA1 Message Date
d423a3eb59 added focused border 2024-12-04 11:14:02 +04:00
9bddd151bb added bloc for create community 2024-12-04 11:05:46 +04:00
0b628c85a5 updated create community dialog to mange edit/create 2024-12-04 10:36:41 +04:00
7ee335ab1a Bug fixes 2024-12-03 12:01:17 +03:00
d5ad06335b Merge pull request #57 from SyncrowIOT/routines_issues
Routines issues
2024-12-03 01:04:12 +03:00
d9ef5b4574 push fixes and merge 2024-12-03 01:02:13 +03:00
c58f2eb24e fixed autoamtion update 2024-12-03 00:29:59 +03:00
4e94d2df89 Bug fixes 2024-12-03 00:02:52 +03:00
2f5c5d7da1 rule_disable selected & CreateAutomation actionType 2024-12-02 16:49:44 +03:00
ff4ce8ed01 Bug fixes 2024-12-02 03:58:31 +03:00
1aa6b78fb4 Bug fixes 2024-12-01 12:05:19 +03:00
e003d9b8b3 Merge pull request #56 from SyncrowIOT/routines_fixes
Routines fixes
2024-12-01 10:50:54 +03:00
76e44c8910 push changes 2024-12-01 01:07:26 +03:00
02c788d03a merge and push 2024-12-01 00:43:30 +03:00
938f8c6b51 update scene debug 2024-12-01 00:42:00 +03:00
3ce9a313f7 Bug fixes 2024-12-01 00:21:15 +03:00
b732b0b957 Pulled latest changes 2024-11-30 21:37:55 +03:00
fb6b922df4 Added delete scene function 2024-11-30 20:45:25 +03:00
ce6e7700bc push changes 2024-11-30 20:43:21 +03:00
8d388ac482 Merge pull request #55 from SyncrowIOT/bugfix/community-flow
Bugfix/community flow
2024-11-29 18:36:03 +03:00
42c7577b35 validity for space name 2024-11-29 19:03:27 +04:00
ae2b4b9b06 border 2024-11-29 17:31:37 +04:00
98faef2bde typo 2024-11-29 17:30:03 +04:00
edf297c569 border 2024-11-29 17:29:18 +04:00
2cd850b66f border color change to red 2024-11-29 17:26:57 +04:00
3c543b2e0f added validation for community name 2024-11-29 17:22:00 +04:00
a3c6421b0d updated flow for create community 2024-11-29 16:50:49 +04:00
875787919e Merge pull request #54 from SyncrowIOT/bugfix/add-new-community
Bugfix/add new community
2024-11-29 14:41:52 +03:00
51402720dd space load on space save 2024-11-28 22:50:05 +04:00
123291fd89 parse incoming connection 2024-11-28 20:31:19 +04:00
69e3f50269 reduced width of text controller 2024-11-28 20:11:16 +04:00
bd78c8c1cc format 2024-11-28 20:09:28 +04:00
4047664c59 create community should move back to blank page 2024-11-28 20:07:18 +04:00
084f4fbda8 updated highlighted space 2024-11-28 20:02:41 +04:00
817671dbeb updated deselect on structure 2024-11-28 20:01:14 +04:00
660b5aaf6c remove unused code 2024-11-28 19:56:40 +04:00
0f500a4efa remove unused state functions 2024-11-28 19:55:45 +04:00
0b2d44fc2b updated LoadedSpaceView 2024-11-28 19:54:04 +04:00
b75f8a3440 side bar widget uses context 2024-11-28 19:52:20 +04:00
d02d680520 add events for select space and community 2024-11-28 19:45:24 +04:00
68ae992963 added select space and select community event 2024-11-28 19:40:44 +04:00
ea20afd34e fixed issue of not recursively calling communtiy list 2024-11-28 15:16:07 +04:00
b0f0a91cda Merge pull request #49 from SyncrowIOT/feature/space-management
Feature/space management
2024-11-28 12:15:06 +03:00
68 changed files with 2562 additions and 1409 deletions

View File

@ -1,28 +0,0 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'switch_tabs_event.dart';
part 'switch_tabs_state.dart';
class SwitchTabsBloc extends Bloc<SwitchTabsEvent, SwitchTabsState> {
SwitchTabsBloc() : super(SwitchTabsInitial()) {
on<TriggerSwitchTabsEvent>(_switchTab);
on<CreateNewRoutineViewEvent>(_newRoutineView);
}
FutureOr<void> _switchTab(
TriggerSwitchTabsEvent event,
Emitter<SwitchTabsState> emit,
) {
emit(SelectedTabState(event.isRoutineView));
}
FutureOr<void> _newRoutineView(
CreateNewRoutineViewEvent event,
Emitter<SwitchTabsState> emit,
) {
emit(ShowCreateRoutineState(event.showCreateNewRoutineView));
}
}

View File

@ -1,21 +0,0 @@
part of 'switch_tabs_bloc.dart';
sealed class SwitchTabsEvent extends Equatable {
const SwitchTabsEvent();
}
class TriggerSwitchTabsEvent extends SwitchTabsEvent {
final bool isRoutineView;
const TriggerSwitchTabsEvent(this.isRoutineView);
@override
List<Object?> get props => [isRoutineView];
}
class CreateNewRoutineViewEvent extends SwitchTabsEvent {
final bool showCreateNewRoutineView;
const CreateNewRoutineViewEvent(this.showCreateNewRoutineView);
@override
List<Object?> get props => [showCreateNewRoutineView];
}

View File

@ -1,26 +0,0 @@
part of 'switch_tabs_bloc.dart';
sealed class SwitchTabsState extends Equatable {
const SwitchTabsState();
}
final class SwitchTabsInitial extends SwitchTabsState {
@override
List<Object> get props => [];
}
class SelectedTabState extends SwitchTabsState {
final bool selectedTab;
const SelectedTabState(this.selectedTab);
@override
List<Object?> get props => [selectedTab];
}
class ShowCreateRoutineState extends SwitchTabsState {
final bool showCreateRoutine;
const ShowCreateRoutineState(this.showCreateRoutine);
@override
List<Object?> get props => [showCreateRoutine];
}

View File

@ -1,9 +1,9 @@
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/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart';
import 'package:syncrow_web/pages/routiens/view/routines_view.dart'; import 'package:syncrow_web/pages/routiens/view/routines_view.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
@ -18,10 +18,6 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(
create: (context) =>
SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)),
),
BlocProvider( BlocProvider(
create: (context) => DeviceManagementBloc()..add(FetchDevices()), create: (context) => DeviceManagementBloc()..add(FetchDevices()),
), ),
@ -33,8 +29,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
style: Theme.of(context).textTheme.headlineLarge, style: Theme.of(context).textTheme.headlineLarge,
), ),
), ),
centerBody: BlocBuilder<SwitchTabsBloc, SwitchTabsState>( centerBody: BlocBuilder<RoutineBloc, RoutineState>(builder: (context, state) {
builder: (context, state) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -44,20 +39,14 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
), ),
onPressed: () { onPressed: () {
context context
.read<SwitchTabsBloc>() .read<RoutineBloc>()
.add(const TriggerSwitchTabsEvent(false)); .add(const TriggerSwitchTabsEvent(isRoutineTab: false));
}, },
child: Text( child: Text(
'Devices', 'Devices',
style: context.textTheme.titleMedium?.copyWith( style: context.textTheme.titleMedium?.copyWith(
color: color: !state.routineTab ? ColorsManager.whiteColors : ColorsManager.grayColor,
state is SelectedTabState && state.selectedTab == false fontWeight: !state.routineTab ? FontWeight.w700 : FontWeight.w400,
? ColorsManager.whiteColors
: ColorsManager.grayColor,
fontWeight: (state is SelectedTabState) &&
state.selectedTab == false
? FontWeight.w700
: FontWeight.w400,
), ),
), ),
), ),
@ -66,21 +55,13 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
backgroundColor: null, backgroundColor: null,
), ),
onPressed: () { onPressed: () {
context context.read<RoutineBloc>().add(const TriggerSwitchTabsEvent(isRoutineTab: true));
.read<SwitchTabsBloc>()
.add(const TriggerSwitchTabsEvent(true));
}, },
child: Text( child: Text(
'Routines', 'Routines',
style: context.textTheme.titleMedium?.copyWith( style: context.textTheme.titleMedium?.copyWith(
color: color: state.routineTab ? ColorsManager.whiteColors : ColorsManager.grayColor,
(state is SelectedTabState) && state.selectedTab == true fontWeight: state.routineTab ? FontWeight.w700 : FontWeight.w400,
? ColorsManager.whiteColors
: ColorsManager.grayColor,
fontWeight:
(state is SelectedTabState) && state.selectedTab == true
? FontWeight.w700
: FontWeight.w400,
), ),
), ),
), ),
@ -88,13 +69,12 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
); );
}), }),
rightBody: const NavigateHomeGridView(), rightBody: const NavigateHomeGridView(),
scaffoldBody: BlocBuilder<SwitchTabsBloc, SwitchTabsState>( scaffoldBody: BlocBuilder<RoutineBloc, RoutineState>(builder: (context, state) {
builder: (context, state) { if (state.routineTab) {
if (state is SelectedTabState && state.selectedTab) {
return const RoutinesView(); return const RoutinesView();
} }
if (state is ShowCreateRoutineState && state.showCreateRoutine) { if (state.createRoutineView) {
return const CreateNewRoutineView(); return CreateNewRoutineView();
} }
return BlocBuilder<DeviceManagementBloc, DeviceManagementState>( return BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
@ -104,8 +84,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
} else if (deviceState is DeviceManagementLoaded) { } else if (deviceState is DeviceManagementLoaded) {
return DeviceManagementBody(devices: deviceState.devices); return DeviceManagementBody(devices: deviceState.devices);
} else if (deviceState is DeviceManagementFiltered) { } else if (deviceState is DeviceManagementFiltered) {
return DeviceManagementBody( return DeviceManagementBody(devices: deviceState.filteredDevices);
devices: deviceState.filteredDevices);
} else { } else {
return const Center(child: Text('Error fetching Devices')); return const Center(child: Text('Error fetching Devices'));
} }

View File

@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/auth/model/user_model.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/pages/home/bloc/home_state.dart';
import 'package:syncrow_web/pages/home/home_model/home_item_model.dart'; import 'package:syncrow_web/pages/home/home_model/home_item_model.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/services/home_api.dart'; import 'package:syncrow_web/services/home_api.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
@ -41,8 +42,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async { Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
try { try {
var uuid = var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
user = await HomeApi().fetchUserInfo(uuid); user = await HomeApi().fetchUserInfo(uuid);
emit(HomeInitial()); emit(HomeInitial());
} catch (e) { } catch (e) {
@ -84,6 +84,8 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
icon: Assets.devicesIcon, icon: Assets.devicesIcon,
active: true, active: true,
onPress: (context) { onPress: (context) {
BlocProvider.of<RoutineBloc>(context)
.add(const TriggerSwitchTabsEvent(isRoutineTab: false));
context.go(RoutesConst.deviceManagementPage); context.go(RoutesConst.deviceManagementPage);
}, },
color: ColorsManager.primaryColor, color: ColorsManager.primaryColor,

View File

@ -16,6 +16,7 @@ class EffectPeriodBloc extends Bloc<EffectPeriodEvent, EffectPeriodState> {
}; };
EffectPeriodBloc() : super(EffectPeriodState.initial()) { EffectPeriodBloc() : super(EffectPeriodState.initial()) {
on<InitialEffectPeriodEvent>(_initialEvent);
on<SetPeriod>(_onSetPeriod); on<SetPeriod>(_onSetPeriod);
on<ToggleDay>(_onToggleDay); on<ToggleDay>(_onToggleDay);
on<SetCustomTime>(_onSetCustomTime); on<SetCustomTime>(_onSetCustomTime);
@ -24,6 +25,15 @@ class EffectPeriodBloc extends Bloc<EffectPeriodEvent, EffectPeriodState> {
on<SetDays>(_setAllDays); on<SetDays>(_setAllDays);
} }
void _initialEvent(InitialEffectPeriodEvent event, Emitter<EffectPeriodState> emit) {
add(SetCustomTime(event.effectiveTime.start, event.effectiveTime.end));
emit(state.copyWith(
selectedDaysBinary: event.effectiveTime.loops,
customStartTime: event.effectiveTime.start,
customEndTime: event.effectiveTime.end,
));
}
void _onSetPeriod(SetPeriod event, Emitter<EffectPeriodState> emit) { void _onSetPeriod(SetPeriod event, Emitter<EffectPeriodState> emit) {
String startTime = ''; String startTime = '';
String endTime = ''; String endTime = '';

View File

@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart'; import 'package:syncrow_web/utils/constants/app_enum.dart';
abstract class EffectPeriodEvent extends Equatable { abstract class EffectPeriodEvent extends Equatable {
@ -8,6 +9,15 @@ abstract class EffectPeriodEvent extends Equatable {
List<Object> get props => []; List<Object> get props => [];
} }
class InitialEffectPeriodEvent extends EffectPeriodEvent {
final EffectiveTime effectiveTime;
const InitialEffectPeriodEvent(this.effectiveTime);
@override
List<Object> get props => [effectiveTime];
}
class SetPeriod extends EffectPeriodEvent { class SetPeriod extends EffectPeriodEvent {
final EnumEffectivePeriodOptions period; final EnumEffectivePeriodOptions period;

File diff suppressed because it is too large Load Diff

View File

@ -84,8 +84,7 @@ class RemoveDragCard extends RoutineEvent {
final int index; final int index;
final bool isFromThen; final bool isFromThen;
final String key; final String key;
const RemoveDragCard( const RemoveDragCard({required this.index, required this.isFromThen, required this.key});
{required this.index, required this.isFromThen, required this.key});
@override @override
List<Object> get props => [index, isFromThen, key]; List<Object> get props => [index, isFromThen, key];
} }
@ -105,12 +104,10 @@ class EffectiveTimePeriodEvent extends RoutineEvent {
} }
class CreateAutomationEvent extends RoutineEvent { class CreateAutomationEvent extends RoutineEvent {
// final CreateAutomationModel createAutomationModel;
final String? automationId; final String? automationId;
final bool updateAutomation; final bool updateAutomation;
const CreateAutomationEvent({ const CreateAutomationEvent({
//required this.createAutomationModel,
this.automationId, this.automationId,
this.updateAutomation = false, this.updateAutomation = false,
}); });
@ -159,21 +156,55 @@ class InitializeRoutineState extends RoutineEvent {
} }
class DeleteScene extends RoutineEvent { class DeleteScene extends RoutineEvent {
final String sceneId; const DeleteScene();
final String unitUuid;
const DeleteScene({required this.sceneId, required this.unitUuid});
@override @override
List<Object> get props => [sceneId]; List<Object> get props => [];
} }
class DeleteAutomation extends RoutineEvent { // class DeleteAutomation extends RoutineEvent {
final String automationId; // final String automationId;
final String unitUuid; // const DeleteAutomation({required this.automationId});
const DeleteAutomation({required this.automationId, required this.unitUuid}); // @override
// List<Object> get props => [automationId];
// }
class UpdateScene extends RoutineEvent {
const UpdateScene();
@override @override
List<Object> get props => [automationId]; List<Object> get props => [];
} }
class UpdateAutomation extends RoutineEvent {
const UpdateAutomation();
@override
List<Object> get props => [];
}
class SetAutomationActionExecutor extends RoutineEvent {
final String automationActionExecutor;
const SetAutomationActionExecutor({required this.automationActionExecutor});
@override
List<Object> get props => [automationActionExecutor];
}
class TriggerSwitchTabsEvent extends RoutineEvent {
final bool isRoutineTab;
const TriggerSwitchTabsEvent({required this.isRoutineTab});
@override
List<Object> get props => [isRoutineTab];
}
class CreateNewRoutineViewEvent extends RoutineEvent {
final bool createRoutineView;
const CreateNewRoutineViewEvent({required this.createRoutineView});
@override
List<Object> get props => [createRoutineView];
}
class FetchDevicesInRoutine extends RoutineEvent {}
class ResetRoutineState extends RoutineEvent {} class ResetRoutineState extends RoutineEvent {}
class ClearFunctions extends RoutineEvent {} class ClearFunctions extends RoutineEvent {}
class ResetErrorMessage extends RoutineEvent {}

View File

@ -21,9 +21,13 @@ class RoutineState extends Equatable {
final String? sceneId; final String? sceneId;
final String? automationId; final String? automationId;
final bool? isUpdate; final bool? isUpdate;
final List<AllDevicesModel> devices;
// final String? automationActionExecutor;
final bool routineTab;
final bool createRoutineView;
const RoutineState({ const RoutineState(
this.ifItems = const [], {this.ifItems = const [],
this.thenItems = const [], this.thenItems = const [],
this.availableCards = const [], this.availableCards = const [],
this.scenes = const [], this.scenes = const [],
@ -43,7 +47,10 @@ class RoutineState extends Equatable {
this.sceneId, this.sceneId,
this.automationId, this.automationId,
this.isUpdate, this.isUpdate,
}); this.devices = const [],
// this.automationActionExecutor,
this.routineTab = false,
this.createRoutineView = false});
RoutineState copyWith({ RoutineState copyWith({
List<Map<String, dynamic>>? ifItems, List<Map<String, dynamic>>? ifItems,
@ -65,6 +72,11 @@ class RoutineState extends Equatable {
String? sceneId, String? sceneId,
String? automationId, String? automationId,
bool? isUpdate, bool? isUpdate,
List<AllDevicesModel>? devices,
// String? automationActionExecutor,
TextEditingController? nameController,
bool? routineTab,
bool? createRoutineView,
}) { }) {
return RoutineState( return RoutineState(
ifItems: ifItems ?? this.ifItems, ifItems: ifItems ?? this.ifItems,
@ -89,7 +101,10 @@ class RoutineState extends Equatable {
sceneId: sceneId ?? this.sceneId, sceneId: sceneId ?? this.sceneId,
automationId: automationId ?? this.automationId, automationId: automationId ?? this.automationId,
isUpdate: isUpdate ?? this.isUpdate, isUpdate: isUpdate ?? this.isUpdate,
); devices: devices ?? this.devices,
// automationActionExecutor: automationActionExecutor ?? this.automationActionExecutor,
routineTab: routineTab ?? this.routineTab,
createRoutineView: createRoutineView ?? this.createRoutineView);
} }
@override @override
@ -112,6 +127,10 @@ class RoutineState extends Equatable {
effectiveTime, effectiveTime,
sceneId, sceneId,
automationId, automationId,
isUpdate isUpdate,
devices,
// automationActionExecutor,
routineTab,
createRoutineView
]; ];
} }

View File

@ -1,3 +1,5 @@
import 'dart:convert';
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:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@ -6,10 +8,11 @@ import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SaveRoutineHelper { class SaveRoutineHelper {
static Future<bool?> showSaveRoutineDialog(BuildContext context) async { static Future<void> showSaveRoutineDialog(BuildContext context) async {
return showDialog<bool?>( return showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>( return BlocBuilder<RoutineBloc, RoutineState>(
@ -98,18 +101,29 @@ class SaveRoutineHelper {
final functions = final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? []; state.selectedFunctions[item['uniqueCustomId']] ?? [];
return ListTile( return ListTile(
leading: SvgPicture.asset( leading: item['type'] == 'tap_to_run'
? Image.memory(
base64Decode(item['icon']),
width: 22,
height: 22,
)
: SvgPicture.asset(
item['imagePath'], item['imagePath'],
width: 22, width: 22,
height: 22, height: 22,
), ),
title: title: Text(
Text(item['title'], style: const TextStyle(fontSize: 14)), item['title'],
style: context.textTheme.bodySmall?.copyWith(
fontSize: 14,
color: ColorsManager.grayColor,
),
),
subtitle: Wrap( subtitle: Wrap(
children: functions children: functions
.map((f) => Text( .map((f) => Text(
'${f.operationName}: ${f.value}, ', '${f.operationName}: ${f.value}, ',
style: const TextStyle( style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.grayColor, fontSize: 8), color: ColorsManager.grayColor, fontSize: 8),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 3, maxLines: 3,
@ -124,24 +138,33 @@ class SaveRoutineHelper {
], ],
), ),
), ),
if (state.errorMessage != null) // if (state.errorMessage != null || state.errorMessage!.isNotEmpty)
Padding( // Padding(
padding: const EdgeInsets.all(8.0), // padding: const EdgeInsets.all(8.0),
child: Text( // child: Text(
state.errorMessage!, // state.errorMessage!,
style: const TextStyle(color: Colors.red), // style: const TextStyle(color: Colors.red),
), // ),
), // ),
DialogFooter( DialogFooter(
onCancel: () => Navigator.pop(context, false), onCancel: () => Navigator.pop(context),
onConfirm: () { onConfirm: () async {
if (state.isAutomation) { if (state.isAutomation) {
if (state.isUpdate ?? false) {
context.read<RoutineBloc>().add(const UpdateAutomation());
} else {
context.read<RoutineBloc>().add(const CreateAutomationEvent()); context.read<RoutineBloc>().add(const CreateAutomationEvent());
}
} else {
if (state.isUpdate ?? false) {
context.read<RoutineBloc>().add(const UpdateScene());
} else { } else {
context.read<RoutineBloc>().add(const CreateSceneEvent()); context.read<RoutineBloc>().add(const CreateSceneEvent());
} }
}
Navigator.pop(context, true); // if (state.errorMessage == null || state.errorMessage!.isEmpty) {
Navigator.pop(context);
// }
}, },
isConfirmEnabled: true, isConfirmEnabled: true,
), ),

View File

@ -17,7 +17,7 @@ class CreateAutomationModel {
required this.actions, required this.actions,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap([String? automationId]) {
return { return {
'spaceUuid': spaceUuid, 'spaceUuid': spaceUuid,
'automationName': automationName, 'automationName': automationName,
@ -41,7 +41,7 @@ class CreateAutomationModel {
); );
} }
String toJson() => json.encode(toMap()); String toJson(String? automationId) => json.encode(toMap(automationId));
factory CreateAutomationModel.fromJson(String source) => factory CreateAutomationModel.fromJson(String source) =>
CreateAutomationModel.fromMap(json.decode(source)); CreateAutomationModel.fromMap(json.decode(source));
@ -92,7 +92,7 @@ class Condition {
return { return {
'code': code, 'code': code,
'entityId': entityId, 'entityId': entityId,
'entityType': entityType, 'entityType': 'device_report',
'expr': expr.toMap(), 'expr': expr.toMap(),
}; };
} }
@ -137,11 +137,13 @@ class ConditionExpr {
class AutomationAction { class AutomationAction {
String entityId; String entityId;
String? actionType;
String actionExecutor; String actionExecutor;
ExecutorProperty? executorProperty; ExecutorProperty? executorProperty;
AutomationAction({ AutomationAction({
required this.entityId, required this.entityId,
this.actionType,
required this.actionExecutor, required this.actionExecutor,
this.executorProperty, this.executorProperty,
}); });
@ -150,12 +152,15 @@ class AutomationAction {
return { return {
'entityId': entityId, 'entityId': entityId,
'actionExecutor': actionExecutor, 'actionExecutor': actionExecutor,
if (executorProperty != null)
'executorProperty': executorProperty?.toMap(), 'executorProperty': executorProperty?.toMap(),
'actionType': actionType
}; };
} }
factory AutomationAction.fromMap(Map<String, dynamic> map) { factory AutomationAction.fromMap(Map<String, dynamic> map) {
return AutomationAction( return AutomationAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '', entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '', actionExecutor: map['actionExecutor'] ?? '',
executorProperty: map['executorProperty'] != null executorProperty: map['executorProperty'] != null

View File

@ -95,10 +95,12 @@ class CreateSceneModel {
class CreateSceneAction { class CreateSceneAction {
String entityId; String entityId;
String? actionType;
String actionExecutor; String actionExecutor;
CreateSceneExecutorProperty? executorProperty; CreateSceneExecutorProperty? executorProperty;
CreateSceneAction({ CreateSceneAction({
this.actionType,
required this.entityId, required this.entityId,
required this.actionExecutor, required this.actionExecutor,
required this.executorProperty, required this.executorProperty,
@ -110,6 +112,7 @@ class CreateSceneAction {
CreateSceneExecutorProperty? executorProperty, CreateSceneExecutorProperty? executorProperty,
}) { }) {
return CreateSceneAction( return CreateSceneAction(
actionType: actionType ?? this.actionType,
entityId: entityId ?? this.entityId, entityId: entityId ?? this.entityId,
actionExecutor: actionExecutor ?? this.actionExecutor, actionExecutor: actionExecutor ?? this.actionExecutor,
executorProperty: executorProperty ?? this.executorProperty, executorProperty: executorProperty ?? this.executorProperty,
@ -125,6 +128,7 @@ class CreateSceneAction {
}; };
} else { } else {
return { return {
"actionType": actionType,
'entityId': entityId, 'entityId': entityId,
'actionExecutor': actionExecutor, 'actionExecutor': actionExecutor,
}; };
@ -133,6 +137,7 @@ class CreateSceneAction {
factory CreateSceneAction.fromMap(Map<String, dynamic> map) { factory CreateSceneAction.fromMap(Map<String, dynamic> map) {
return CreateSceneAction( return CreateSceneAction(
actionType: map['actionType'],
entityId: map['entityId'] ?? '', entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '', actionExecutor: map['actionExecutor'] ?? '',
executorProperty: executorProperty:

View File

@ -13,6 +13,8 @@ class RoutineDetailsModel {
final EffectiveTime? effectiveTime; final EffectiveTime? effectiveTime;
final List<RoutineCondition>? conditions; final List<RoutineCondition>? conditions;
final String? type; final String? type;
final String? sceneId;
final String? automationId;
RoutineDetailsModel({ RoutineDetailsModel({
required this.spaceUuid, required this.spaceUuid,
@ -24,6 +26,8 @@ class RoutineDetailsModel {
this.effectiveTime, this.effectiveTime,
this.conditions, this.conditions,
this.type, this.type,
this.sceneId,
this.automationId,
}); });
// Convert to CreateSceneModel // Convert to CreateSceneModel
@ -57,12 +61,14 @@ class RoutineDetailsModel {
'name': name, 'name': name,
'decisionExpr': decisionExpr, 'decisionExpr': decisionExpr,
'actions': actions.map((x) => x.toMap()).toList(), 'actions': actions.map((x) => x.toMap()).toList(),
if (iconId != null) 'iconId': iconId, if (iconId != null) 'iconUuid': iconId,
if (showInDevice != null) 'showInDevice': showInDevice, if (showInDevice != null) 'showInDevice': showInDevice,
if (effectiveTime != null) 'effectiveTime': effectiveTime!.toMap(), if (effectiveTime != null) 'effectiveTime': effectiveTime!.toMap(),
if (conditions != null) if (conditions != null)
'conditions': conditions!.map((x) => x.toMap()).toList(), 'conditions': conditions!.map((x) => x.toMap()).toList(),
if (type != null) 'type': type, if (type != null) 'type': type,
if (sceneId != null) 'sceneId': sceneId,
if (automationId != null) 'automationId': automationId,
}; };
} }
@ -74,7 +80,7 @@ class RoutineDetailsModel {
actions: List<RoutineAction>.from( actions: List<RoutineAction>.from(
map['actions']?.map((x) => RoutineAction.fromMap(x)) ?? [], map['actions']?.map((x) => RoutineAction.fromMap(x)) ?? [],
), ),
iconId: map['iconId'], iconId: map['iconUuid'],
showInDevice: map['showInDevice'], showInDevice: map['showInDevice'],
effectiveTime: map['effectiveTime'] != null effectiveTime: map['effectiveTime'] != null
? EffectiveTime.fromMap(map['effectiveTime']) ? EffectiveTime.fromMap(map['effectiveTime'])
@ -84,6 +90,8 @@ class RoutineDetailsModel {
map['conditions'].map((x) => RoutineCondition.fromMap(x))) map['conditions'].map((x) => RoutineCondition.fromMap(x)))
: null, : null,
type: map['type'], type: map['type'],
sceneId: map['sceneId'],
automationId: map['automationId'],
); );
} }
@ -96,12 +104,18 @@ class RoutineDetailsModel {
class RoutineAction { class RoutineAction {
final String entityId; final String entityId;
final String actionExecutor; final String actionExecutor;
final String? name;
final RoutineExecutorProperty? executorProperty; final RoutineExecutorProperty? executorProperty;
final String productType;
final String? type;
RoutineAction({ RoutineAction({
required this.entityId, required this.entityId,
required this.actionExecutor, required this.actionExecutor,
required this.productType,
this.executorProperty, this.executorProperty,
this.name,
this.type,
}); });
CreateSceneAction toCreateSceneAction() { CreateSceneAction toCreateSceneAction() {
@ -124,6 +138,8 @@ class RoutineAction {
return { return {
'entityId': entityId, 'entityId': entityId,
'actionExecutor': actionExecutor, 'actionExecutor': actionExecutor,
if (type != null) 'type': type,
if (name != null) 'name': name,
if (executorProperty != null) if (executorProperty != null)
'executorProperty': executorProperty!.toMap(), 'executorProperty': executorProperty!.toMap(),
}; };
@ -133,6 +149,9 @@ class RoutineAction {
return RoutineAction( return RoutineAction(
entityId: map['entityId'] ?? '', entityId: map['entityId'] ?? '',
actionExecutor: map['actionExecutor'] ?? '', actionExecutor: map['actionExecutor'] ?? '',
productType: map['productType'] ?? '',
name: map['name'] ?? '',
type: map['type'] ?? '',
executorProperty: map['executorProperty'] != null executorProperty: map['executorProperty'] != null
? RoutineExecutorProperty.fromMap(map['executorProperty']) ? RoutineExecutorProperty.fromMap(map['executorProperty'])
: null, : null,
@ -189,12 +208,14 @@ class RoutineCondition {
final String entityId; final String entityId;
final String entityType; final String entityType;
final RoutineConditionExpr expr; final RoutineConditionExpr expr;
final String productType;
RoutineCondition({ RoutineCondition({
required this.code, required this.code,
required this.entityId, required this.entityId,
required this.entityType, required this.entityType,
required this.expr, required this.expr,
required this.productType,
}); });
Condition toCondition() { Condition toCondition() {
@ -221,6 +242,7 @@ class RoutineCondition {
entityId: map['entityId'] ?? '', entityId: map['entityId'] ?? '',
entityType: map['entityType'] ?? '', entityType: map['entityType'] ?? '',
expr: RoutineConditionExpr.fromMap(map['expr']), expr: RoutineConditionExpr.fromMap(map['expr']),
productType: map['productType'] ?? '',
); );
} }
} }

View File

@ -16,6 +16,7 @@ class CreateNewRoutineView extends StatelessWidget {
this.routineId, this.routineId,
this.isScene = true, this.isScene = true,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(

View File

@ -1,20 +1,30 @@
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/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart';
import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/fetch_routine_scenes_automation.dart'; import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/fetch_routine_scenes_automation.dart';
import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/routine_view_card.dart'; import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/routine_view_card.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class RoutinesView extends StatelessWidget { class RoutinesView extends StatefulWidget {
const RoutinesView({super.key}); const RoutinesView({super.key});
@override
State<RoutinesView> createState() => _RoutinesViewState();
}
class _RoutinesViewState extends State<RoutinesView> {
@override
void initState() {
super.initState();
context.read<RoutineBloc>().add(FetchDevicesInRoutine());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SwitchTabsBloc, SwitchTabsState>( return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) { builder: (context, state) {
if (state is ShowCreateRoutineState && state.showCreateRoutine) { if (state.createRoutineView) {
return const CreateNewRoutineView(); return const CreateNewRoutineView();
} }
return Padding( return Padding(
@ -36,12 +46,12 @@ class RoutinesView extends StatelessWidget {
), ),
RoutineViewCard( RoutineViewCard(
onTap: () { onTap: () {
BlocProvider.of<SwitchTabsBloc>(context).add(
const CreateNewRoutineViewEvent(true),
);
context.read<RoutineBloc>().add( context.read<RoutineBloc>().add(
(ResetRoutineState()), (ResetRoutineState()),
); );
BlocProvider.of<RoutineBloc>(context).add(
const CreateNewRoutineViewEvent(createRoutineView: true),
);
}, },
icon: Icons.add, icon: Icons.add,
textString: '', textString: '',

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/custom_dialog.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DeleteSceneWidget extends StatelessWidget {
const DeleteSceneWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
GestureDetector(
onTap: () async {
await showCustomDialog(
context: context,
message: 'Are you sure you want to delete this scene?',
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'Cancel',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.textGray,
),
),
),
),
Container(width: 1, height: 50, color: ColorsManager.greyColor),
InkWell(
onTap: () {
context.read<RoutineBloc>().add(const DeleteScene());
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'Confirm',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
),
),
],
),
]);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.delete,
color: ColorsManager.red,
),
const SizedBox(
width: 2,
),
Text(
'Delete',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.red,
),
),
],
),
),
const SizedBox(
height: 10,
),
],
);
}
}

View File

@ -35,6 +35,20 @@ class DraggableCard extends StatelessWidget {
final deviceFunctions = final deviceFunctions =
state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; state.selectedFunctions[deviceData['uniqueCustomId']] ?? [];
int index = state.thenItems.indexWhere(
(item) => item['uniqueCustomId'] == deviceData['uniqueCustomId']);
if (index != -1) {
return _buildCardContent(context, deviceFunctions, padding: padding);
}
int ifIndex = state.ifItems.indexWhere(
(item) => item['uniqueCustomId'] == deviceData['uniqueCustomId']);
if (ifIndex != -1) {
return _buildCardContent(context, deviceFunctions, padding: padding);
}
return Draggable<Map<String, dynamic>>( return Draggable<Map<String, dynamic>>(
data: deviceData, data: deviceData,
feedback: Transform.rotate( feedback: Transform.rotate(
@ -79,19 +93,19 @@ class DraggableCard extends StatelessWidget {
), ),
), ),
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: imagePath.contains('.svg') child: deviceData['type'] == 'tap_to_run'
? SvgPicture.asset( ? Image.memory(
imagePath, base64Decode(deviceData['icon']),
) )
: Image.memory( : SvgPicture.asset(
base64Decode(imagePath), imagePath,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 3), padding: const EdgeInsets.symmetric(horizontal: 3),
child: Text( child: Text(
title, deviceData['title'] ?? deviceData['name'] ?? title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
@ -104,7 +118,6 @@ class DraggableCard extends StatelessWidget {
], ],
), ),
if (deviceFunctions.isNotEmpty) if (deviceFunctions.isNotEmpty)
// const Divider(height: 1),
...deviceFunctions.map((function) => Row( ...deviceFunctions.map((function) => Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -11,8 +11,7 @@ class FetchRoutineScenesAutomation extends StatefulWidget {
const FetchRoutineScenesAutomation({super.key}); const FetchRoutineScenesAutomation({super.key});
@override @override
State<FetchRoutineScenesAutomation> createState() => State<FetchRoutineScenesAutomation> createState() => _FetchRoutineScenesState();
_FetchRoutineScenesState();
} }
class _FetchRoutineScenesState extends State<FetchRoutineScenesAutomation> class _FetchRoutineScenesState extends State<FetchRoutineScenesAutomation>
@ -67,49 +66,23 @@ class _FetchRoutineScenesState extends State<FetchRoutineScenesAutomation>
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: isSmallScreenSize(context) ? 4.0 : 8.0, right: isSmallScreenSize(context) ? 4.0 : 8.0,
), ),
child: Stack( child: RoutineViewCard(
children: [ onTap: () {
RoutineViewCard( BlocProvider.of<RoutineBloc>(context).add(
onTap: () {}, const CreateNewRoutineViewEvent(createRoutineView: true),
textString: state.scenes[index].name, );
icon: state.scenes[index].icon ?? context.read<RoutineBloc>().add(
Assets.logoHorizontal, GetSceneDetails(
isFromScenes: true,
iconInBytes:
state.scenes[index].iconInBytes,
),
Positioned(
top: 0,
right: 0,
child: InkWell(
onTap: () => context
.read<RoutineBloc>()
.add(
DeleteScene(
sceneId: state.scenes[index].id, sceneId: state.scenes[index].id,
unitUuid: spaceId, isTabToRun: true,
isUpdate: true,
), ),
), );
child: Container( },
height: 20, textString: state.scenes[index].name,
width: 20, icon: state.scenes[index].icon ?? Assets.logoHorizontal,
decoration: BoxDecoration( isFromScenes: true,
color: ColorsManager.whiteColors, iconInBytes: state.scenes[index].iconInBytes,
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.grayColor,
width: 2.0,
),
),
child: const Center(
child: Icon(Icons.delete,
size: 15,
color: ColorsManager.grayColor),
),
),
),
),
],
), ),
), ),
), ),
@ -142,46 +115,20 @@ class _FetchRoutineScenesState extends State<FetchRoutineScenesAutomation>
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: isSmallScreenSize(context) ? 4.0 : 8.0, right: isSmallScreenSize(context) ? 4.0 : 8.0,
), ),
child: Stack( child: RoutineViewCard(
children: [ onTap: () {
RoutineViewCard( BlocProvider.of<RoutineBloc>(context).add(
onTap: () {}, const CreateNewRoutineViewEvent(createRoutineView: true),
textString: state.automations[index].name, );
icon: state.automations[index].icon ??
Assets.automation,
),
Positioned(
top: 0,
right: 0,
child: InkWell(
onTap: () =>
context.read<RoutineBloc>().add( context.read<RoutineBloc>().add(
DeleteAutomation( GetAutomationDetails(
automationId: state automationId: state.automations[index].id,
.automations[index].id, isAutomation: true,
unitUuid: spaceId, isUpdate: true),
), );
), },
child: Container( textString: state.automations[index].name,
height: 20, icon: state.automations[index].icon ?? Assets.automation,
width: 20,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.grayColor,
width: 2.0,
),
),
child: const Center(
child: Icon(Icons.delete,
size: 15,
color: ColorsManager.grayColor),
),
),
),
),
],
), ),
), ),
), ),

View File

@ -1,6 +1,5 @@
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/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart';
@ -10,11 +9,18 @@ class RoutineDevices extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocBuilder<RoutineBloc, RoutineState>(
create: (context) => DeviceManagementBloc()..add(FetchDevices()),
child: BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
builder: (context, state) { builder: (context, state) {
if (state is DeviceManagementLoaded) { if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
Future.delayed(const Duration(seconds: 1), () {
if (state.devices.isEmpty) {
return const Center(child: Text('No devices found'));
}
});
List<AllDevicesModel> deviceList = state.devices List<AllDevicesModel> deviceList = state.devices
.where((device) => .where((device) =>
device.productType == 'AC' || device.productType == 'AC' ||
@ -23,18 +29,15 @@ class RoutineDevices extends StatelessWidget {
device.productType == '3G') device.productType == '3G')
.toList(); .toList();
// Provide the RoutineBloc to the child widgets
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, routineState) {
return Wrap( return Wrap(
spacing: 10, spacing: 10,
runSpacing: 10, runSpacing: 10,
children: deviceList.asMap().entries.map((entry) { children: deviceList.asMap().entries.map((entry) {
final device = entry.value; final device = entry.value;
if (routineState.searchText != null && routineState.searchText!.isNotEmpty) { if (state.searchText != null && state.searchText!.isNotEmpty) {
return device.name! return device.name!
.toLowerCase() .toLowerCase()
.contains(routineState.searchText!.toLowerCase()) .contains(state.searchText!.toLowerCase())
? DraggableCard( ? DraggableCard(
imagePath: device.getDefaultIcon(device.productType), imagePath: device.getDefaultIcon(device.productType),
title: device.name ?? '', title: device.name ?? '',
@ -69,9 +72,4 @@ class RoutineDevices extends StatelessWidget {
}, },
); );
} }
return const Center(child: CircularProgressIndicator());
},
),
);
}
} }

View File

@ -11,20 +11,36 @@ class AutomationDialog extends StatefulWidget {
final String automationName; final String automationName;
final String automationId; final String automationId;
final String uniqueCustomId; final String uniqueCustomId;
final String? passedAutomationActionExecutor;
const AutomationDialog({ const AutomationDialog({
super.key, super.key,
required this.automationName, required this.automationName,
required this.automationId, required this.automationId,
required this.uniqueCustomId, required this.uniqueCustomId,
this.passedAutomationActionExecutor,
}); });
@override @override
_AutomationDialogState createState() => _AutomationDialogState(); State<AutomationDialog> createState() => _AutomationDialogState();
} }
class _AutomationDialogState extends State<AutomationDialog> { class _AutomationDialogState extends State<AutomationDialog> {
bool _isEnabled = true; String? selectedAutomationActionExecutor;
@override
void initState() {
super.initState();
List<DeviceFunctionData>? functions = context
.read<RoutineBloc>()
.state
.selectedFunctions[widget.uniqueCustomId];
for (DeviceFunctionData data in functions ?? []) {
if (data.entityId == widget.automationId) {
selectedAutomationActionExecutor = data.value;
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -41,26 +57,25 @@ class _AutomationDialogState extends State<AutomationDialog> {
ListTile( ListTile(
leading: SvgPicture.asset(Assets.acPower, width: 24, height: 24), leading: SvgPicture.asset(Assets.acPower, width: 24, height: 24),
title: const Text('Enable'), title: const Text('Enable'),
trailing: Radio<bool>( trailing: Radio<String?>(
value: true, value: 'rule_enable',
groupValue: _isEnabled, groupValue: selectedAutomationActionExecutor,
onChanged: (bool? value) { onChanged: (String? value) {
setState(() { setState(() {
_isEnabled = value!; selectedAutomationActionExecutor = 'rule_enable';
}); });
}, }),
),
), ),
ListTile( ListTile(
leading: leading:
SvgPicture.asset(Assets.acPowerOff, width: 24, height: 24), SvgPicture.asset(Assets.acPowerOff, width: 24, height: 24),
title: const Text('Disable'), title: const Text('Disable'),
trailing: Radio<bool>( trailing: Radio<String?>(
value: false, value: 'rule_disable',
groupValue: _isEnabled, groupValue: selectedAutomationActionExecutor,
onChanged: (bool? value) { onChanged: (String? value) {
setState(() { setState(() {
_isEnabled = value!; selectedAutomationActionExecutor = 'rule_disable';
}); });
}, },
), ),
@ -68,23 +83,25 @@ class _AutomationDialogState extends State<AutomationDialog> {
const SizedBox(height: 16), const SizedBox(height: 16),
DialogFooter( DialogFooter(
onConfirm: () { onConfirm: () {
if (selectedAutomationActionExecutor != null) {
context.read<RoutineBloc>().add( context.read<RoutineBloc>().add(
AddFunctionToRoutine( AddFunctionToRoutine(
[ [
DeviceFunctionData( DeviceFunctionData(
entityId: widget.automationId, entityId: widget.automationId,
functionCode: 'automation', functionCode: 'automation',
value: _isEnabled ? 'rule_enable' : 'rule_disable', value: selectedAutomationActionExecutor,
operationName: 'Automation', operationName: 'Automation',
), ),
], ],
widget.uniqueCustomId, widget.uniqueCustomId,
), ),
); );
}
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
onCancel: () => Navigator.of(context).pop(false), onCancel: () => Navigator.of(context).pop(),
isConfirmEnabled: true, isConfirmEnabled: selectedAutomationActionExecutor != null,
dialogWidth: 400, dialogWidth: 400,
), ),
], ],

View File

@ -1,6 +1,5 @@
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/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -49,11 +48,11 @@ class DiscardDialog {
onConfirm: () { onConfirm: () {
context.read<RoutineBloc>().add(ResetRoutineState()); context.read<RoutineBloc>().add(ResetRoutineState());
Navigator.pop(context); Navigator.pop(context);
BlocProvider.of<SwitchTabsBloc>(context).add( BlocProvider.of<RoutineBloc>(context).add(
const CreateNewRoutineViewEvent(false), const CreateNewRoutineViewEvent(createRoutineView: false),
); );
BlocProvider.of<SwitchTabsBloc>(context).add( BlocProvider.of<RoutineBloc>(context).add(
const TriggerSwitchTabsEvent(true), const TriggerSwitchTabsEvent(isRoutineTab: true),
); );
}); });
} }

View File

@ -1,6 +1,7 @@
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/routiens/bloc/effective_period/effect_period_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart';
import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_bloc.dart';
@ -9,6 +10,7 @@ import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_state.dart'
import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart';
import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; import 'package:syncrow_web/pages/routiens/models/icon_model.dart';
import 'package:syncrow_web/pages/routiens/view/effective_period_view.dart'; import 'package:syncrow_web/pages/routiens/view/effective_period_view.dart';
import 'package:syncrow_web/pages/routiens/widgets/delete_scene.dart';
import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -22,8 +24,15 @@ class SettingHelper {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
final isAutomation = context.read<RoutineBloc>().state.isAutomation; final isAutomation = context.read<RoutineBloc>().state.isAutomation;
final effectiveTime = context.read<RoutineBloc>().state.effectiveTime;
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
if (effectiveTime != null)
BlocProvider(
create: (_) => EffectPeriodBloc()..add(InitialEffectPeriodEvent(effectiveTime)),
),
if (effectiveTime == null)
BlocProvider( BlocProvider(
create: (_) => EffectPeriodBloc(), create: (_) => EffectPeriodBloc(),
), ),
@ -44,7 +53,7 @@ class SettingHelper {
} }
return Container( return Container(
width: context.read<SettingBloc>().isExpanded ? 800 : 400, width: context.read<SettingBloc>().isExpanded ? 800 : 400,
height: context.read<SettingBloc>().isExpanded && isAutomation ? 500 : 300, height: context.read<SettingBloc>().isExpanded && isAutomation ? 500 : 350,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@ -167,6 +176,9 @@ class SettingHelper {
fontSize: 14)), fontSize: 14)),
], ],
), ),
if (context.read<RoutineBloc>().state.isUpdate ??
false)
const DeleteSceneWidget()
], ],
)), )),
], ],
@ -284,6 +296,9 @@ class SettingHelper {
fontSize: 14)), fontSize: 14)),
], ],
), ),
if (context.read<RoutineBloc>().state.isUpdate ??
false)
const DeleteSceneWidget()
], ],
)), )),
], ],

View File

@ -1,7 +1,6 @@
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/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart';
import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routiens/helper/save_routine_helper.dart'; import 'package:syncrow_web/pages/routiens/helper/save_routine_helper.dart';
import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/discard_dialog.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/discard_dialog.dart';
@ -10,20 +9,47 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
class RoutineSearchAndButtons extends StatelessWidget { class RoutineSearchAndButtons extends StatefulWidget {
const RoutineSearchAndButtons({ const RoutineSearchAndButtons({
super.key, super.key,
}); });
@override
State<RoutineSearchAndButtons> createState() => _RoutineSearchAndButtonsState();
}
class _RoutineSearchAndButtonsState extends State<RoutineSearchAndButtons> {
late TextEditingController _nameController;
@override
void initState() {
super.initState();
_nameController = TextEditingController();
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>( return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) { builder: (context, state) {
_nameController.text = state.routineName ?? '';
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return Wrap( return Wrap(
runSpacing: 16, runSpacing: 16,
children: [ children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
state.errorMessage ?? '',
style: const TextStyle(color: Colors.red),
),
),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@ -37,24 +63,6 @@ class RoutineSearchAndButtons extends StatelessWidget {
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: maxWidth:
constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32),
// child: StatefulTextField(
// title: 'Routine Name',
// initialValue: state.routineName ?? '',
// height: 40,
// // controller: TextEditingController(),
// hintText: 'Please enter the name',
// boxDecoration: containerWhiteDecoration,
// elevation: 0,
// borderRadius: 15,
// isRequired: true,
// width: 450,
// onSubmitted: (value) {
// // context.read<RoutineBloc>().add(SetRoutineName(value));
// },
// onChanged: (value) {
// context.read<RoutineBloc>().add(SetRoutineName(value));
// },
// ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -81,7 +89,7 @@ class RoutineSearchAndButtons extends StatelessWidget {
child: TextFormField( child: TextFormField(
style: context.textTheme.bodyMedium! style: context.textTheme.bodyMedium!
.copyWith(color: ColorsManager.blackColor), .copyWith(color: ColorsManager.blackColor),
initialValue: state.routineName, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Please enter the name', hintText: 'Please enter the name',
hintStyle: context.textTheme.bodyMedium! hintStyle: context.textTheme.bodyMedium!
@ -90,8 +98,10 @@ class RoutineSearchAndButtons extends StatelessWidget {
const EdgeInsets.symmetric(horizontal: 12, vertical: 10), const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: InputBorder.none, border: InputBorder.none,
), ),
onChanged: (value) { onTapOutside: (_) {
context.read<RoutineBloc>().add(SetRoutineName(value)); context
.read<RoutineBloc>()
.add(SetRoutineName(_nameController.text));
}, },
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@ -115,6 +125,7 @@ class RoutineSearchAndButtons extends StatelessWidget {
? () async { ? () async {
final result = await SettingHelper.showSettingDialog( final result = await SettingHelper.showSettingDialog(
context: context, context: context,
iconId: state.selectedIcon ?? '',
); );
if (result != null) { if (result != null) {
context context
@ -208,16 +219,18 @@ class RoutineSearchAndButtons extends StatelessWidget {
); );
return; return;
} }
final result = // final result =
await SaveRoutineHelper.showSaveRoutineDialog(context); // await
if (result != null && result) { BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
BlocProvider.of<SwitchTabsBloc>(context).add( SaveRoutineHelper.showSaveRoutineDialog(context);
const CreateNewRoutineViewEvent(false), // if (result != null && result) {
); // BlocProvider.of<RoutineBloc>(context).add(
BlocProvider.of<SwitchTabsBloc>(context).add( // const CreateNewRoutineViewEvent(createRoutineView: false),
const TriggerSwitchTabsEvent(true), // );
); // BlocProvider.of<RoutineBloc>(context).add(
} // const TriggerSwitchTabsEvent(isRoutineTab: true),
// );
// }
}, },
borderRadius: 15, borderRadius: 15,
elevation: 0, elevation: 0,
@ -249,8 +262,7 @@ class RoutineSearchAndButtons extends StatelessWidget {
onPressed: state.isAutomation || state.isTabToRun onPressed: state.isAutomation || state.isTabToRun
? () async { ? () async {
final result = await SettingHelper.showSettingDialog( final result = await SettingHelper.showSettingDialog(
context: context, context: context, iconId: state.selectedIcon ?? '');
);
if (result != null) { if (result != null) {
context.read<RoutineBloc>().add(AddSelectedIcon(result)); context.read<RoutineBloc>().add(AddSelectedIcon(result));
} }
@ -301,7 +313,7 @@ class RoutineSearchAndButtons extends StatelessWidget {
width: 200, width: 200,
child: Center( child: Center(
child: DefaultButton( child: DefaultButton(
onPressed: () { onPressed: () async {
if (state.routineName == null || state.routineName!.isEmpty) { if (state.routineName == null || state.routineName!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -335,7 +347,18 @@ class RoutineSearchAndButtons extends StatelessWidget {
); );
return; return;
} }
// final result =
// await
BlocProvider.of<RoutineBloc>(context).add(ResetErrorMessage());
SaveRoutineHelper.showSaveRoutineDialog(context); SaveRoutineHelper.showSaveRoutineDialog(context);
// if (result != null && result) {
// BlocProvider.of<RoutineBloc>(context).add(
// const CreateNewRoutineViewEvent(createRoutineView: false),
// );
// BlocProvider.of<RoutineBloc>(context).add(
// const TriggerSwitchTabsEvent(isRoutineTab: true),
// );
// }
}, },
borderRadius: 15, borderRadius: 15,
elevation: 0, elevation: 0,

View File

@ -30,18 +30,23 @@ class ThenContainer extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)), fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16), const SizedBox(height: 16),
Wrap( state.isLoading && state.isUpdate == true
? const Center(
child: CircularProgressIndicator(),
)
: Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: List.generate( children: List.generate(
state.thenItems.length, state.thenItems.length,
(index) => GestureDetector( (index) => GestureDetector(
onTap: () async { onTap: () async {
if (state.thenItems[index]['deviceId'] == if (state.thenItems[index]
['deviceId'] ==
'delay') { 'delay') {
final result = await DelayHelper final result = await DelayHelper
.showDelayPickerDialog( .showDelayPickerDialog(context,
context, state.thenItems[index]); state.thenItems[index]);
if (result != null) { if (result != null) {
context context
@ -55,6 +60,43 @@ class ThenContainer extends StatelessWidget {
return; return;
} }
if (state.thenItems[index]['type'] ==
'automation') {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) =>
AutomationDialog(
automationName:
state.thenItems[index]
['name'] ??
'Automation',
automationId:
state.thenItems[index]
['deviceId'] ??
'',
uniqueCustomId:
state.thenItems[index]
['uniqueCustomId'],
),
);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToThenContainer({
...state.thenItems[index],
'imagePath':
Assets.automation,
'title':
state.thenItems[index]
['name'] ??
state.thenItems[index]
['title'],
}));
}
return;
}
final result = await DeviceDialogHelper final result = await DeviceDialogHelper
.showDeviceDialog( .showDeviceDialog(
context, state.thenItems[index], context, state.thenItems[index],
@ -76,8 +118,9 @@ class ThenContainer extends StatelessWidget {
imagePath: state.thenItems[index] imagePath: state.thenItems[index]
['imagePath'] ?? ['imagePath'] ??
'', '',
title: title: state.thenItems[index]
state.thenItems[index]['title'] ?? '', ['title'] ??
'',
deviceData: state.thenItems[index], deviceData: state.thenItems[index],
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 8), horizontal: 4, vertical: 8),
@ -109,6 +152,12 @@ class ThenContainer extends StatelessWidget {
} }
if (mutableData['type'] == 'automation') { if (mutableData['type'] == 'automation') {
int index = state.thenItems.indexWhere(
(item) => item['deviceId'] == mutableData['deviceId']);
if (index != -1) {
return;
}
final result = await showDialog<bool>( final result = await showDialog<bool>(
context: context, context: context,
builder: (BuildContext context) => AutomationDialog( builder: (BuildContext context) => AutomationDialog(
@ -129,9 +178,14 @@ class ThenContainer extends StatelessWidget {
} }
if (mutableData['type'] == 'tap_to_run' && state.isAutomation) { if (mutableData['type'] == 'tap_to_run' && state.isAutomation) {
int index = state.thenItems.indexWhere(
(item) => item['deviceId'] == mutableData['deviceId']);
if (index != -1) {
return;
}
context.read<RoutineBloc>().add(AddToThenContainer({ context.read<RoutineBloc>().add(AddToThenContainer({
...mutableData, ...mutableData,
'imagePath': Assets.logo, 'imagePath': mutableData['imagePath'] ?? Assets.logo,
'title': mutableData['name'], 'title': mutableData['name'],
})); }));

View File

@ -1,26 +1,31 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_state.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/product_api.dart';
import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/services/space_mana_api.dart';
class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementState> { class SpaceManagementBloc
extends Bloc<SpaceManagementEvent, SpaceManagementState> {
final CommunitySpaceManagementApi _api; final CommunitySpaceManagementApi _api;
final ProductApi _productApi; final ProductApi _productApi;
List<ProductModel>? _cachedProducts; List<ProductModel>? _cachedProducts;
SpaceManagementBloc(this._api, this._productApi) : super(SpaceManagementInitial()) { SpaceManagementBloc(this._api, this._productApi)
: super(SpaceManagementInitial()) {
on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces); on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces);
on<UpdateSpacePositionEvent>(_onUpdateSpacePosition); on<UpdateSpacePositionEvent>(_onUpdateSpacePosition);
on<CreateCommunityEvent>(_onCreateCommunity); on<CreateCommunityEvent>(_onCreateCommunity);
on<SaveSpacesEvent>(_onSaveSpaces); on<SelectCommunityEvent>(_onSelectCommunity);
on<FetchProductsEvent>(_onFetchProducts);
on<DeleteCommunityEvent>(_onCommunityDelete); on<DeleteCommunityEvent>(_onCommunityDelete);
on<UpdateCommunityEvent>(_onUpdateCommunity); on<UpdateCommunityEvent>(_onUpdateCommunity);
on<SaveSpacesEvent>(_onSaveSpaces);
on<FetchProductsEvent>(_onFetchProducts);
on<SelectSpaceEvent>(_onSelectSpace);
on<NewCommunityEvent>(_onNewCommunity);
} }
void _onUpdateCommunity( void _onUpdateCommunity(
@ -30,10 +35,12 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
final previousState = state; final previousState = state;
try { try {
emit(SpaceManagementLoading()); emit(SpaceManagementLoading());
final success = await _api.updateCommunity(event.communityUuid, event.name); final success =
await _api.updateCommunity(event.communityUuid, event.name);
if (success) { if (success) {
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded) {
final updatedCommunities = List<CommunityModel>.from(previousState.communities); final updatedCommunities =
List<CommunityModel>.from(previousState.communities);
for (var community in updatedCommunities) { for (var community in updatedCommunities) {
if (community.uuid == event.communityUuid) { if (community.uuid == event.communityUuid) {
community.name = event.name; community.name = event.name;
@ -45,7 +52,6 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
products: previousState.products, products: previousState.products,
selectedCommunity: previousState.selectedCommunity, selectedCommunity: previousState.selectedCommunity,
)); ));
} }
} else { } else {
emit(const SpaceManagementError('Failed to update the community.')); emit(const SpaceManagementError('Failed to update the community.'));
@ -55,20 +61,45 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} }
} }
void _onloadProducts() async {
if (_cachedProducts == null) {
final products = await _productApi.fetchProducts();
_cachedProducts = products;
}
}
void _onFetchProducts( void _onFetchProducts(
FetchProductsEvent event, FetchProductsEvent event,
Emitter<SpaceManagementState> emit, Emitter<SpaceManagementState> emit,
) async { ) async {
if (_cachedProducts != null) { try {
// Products are already cached, no need to fetch again _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; return;
} }
try { emit(BlankState(
final products = await _productApi.fetchProducts(); communities: event.communities,
_cachedProducts = products; // Cache the products locally products: _cachedProducts ?? [],
} catch (e) { ));
emit(SpaceManagementError('Error fetching products: $e')); } catch (error) {
emit(SpaceManagementError('Error loading communities: $error'));
} }
} }
@ -78,17 +109,13 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
) async { ) async {
emit(SpaceManagementLoading()); emit(SpaceManagementLoading());
try { try {
if (_cachedProducts == null) { _onloadProducts();
final products = await _productApi.fetchProducts();
_cachedProducts = products;
}
// Fetch all communities
List<CommunityModel> communities = await _api.fetchCommunities(); List<CommunityModel> communities = await _api.fetchCommunities();
List<CommunityModel> updatedCommunities = await Future.wait( List<CommunityModel> updatedCommunities = await Future.wait(
communities.map((community) async { communities.map((community) async {
List<SpaceModel> spaces = await _api.getSpaceHierarchy(community.uuid); List<SpaceModel> spaces =
await _fetchSpacesForCommunity(community.uuid);
return CommunityModel( return CommunityModel(
uuid: community.uuid, uuid: community.uuid,
createdAt: community.createdAt, createdAt: community.createdAt,
@ -101,7 +128,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
}).toList(), }).toList(),
); );
emit(SpaceManagementLoaded(communities: updatedCommunities, products: _cachedProducts ?? [])); emit(SpaceManagementLoaded(
communities: updatedCommunities, products: _cachedProducts ?? []));
} catch (e) { } catch (e) {
emit(SpaceManagementError('Error loading communities and spaces: $e')); emit(SpaceManagementError('Error loading communities and spaces: $e'));
} }
@ -139,16 +167,21 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
emit(SpaceManagementLoading()); emit(SpaceManagementLoading());
try { try {
CommunityModel? newCommunity = await _api.createCommunity(event.name, event.description); CommunityModel? newCommunity =
await _api.createCommunity(event.name, event.description);
if (newCommunity != null) { if (newCommunity != null) {
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded ||
final updatedCommunities = List<CommunityModel>.from(previousState.communities) previousState is BlankState) {
..add(newCommunity); final prevCommunities = List<CommunityModel>.from(
(previousState as dynamic).communities,
);
final updatedCommunities = prevCommunities..add(newCommunity);
emit(SpaceManagementLoaded( emit(SpaceManagementLoaded(
communities: updatedCommunities, communities: updatedCommunities,
products: _cachedProducts ?? [], products: _cachedProducts ?? [],
selectedCommunity: newCommunity)); selectedCommunity: newCommunity,
selectedSpace: null));
} }
} else { } else {
emit(const SpaceManagementError('Error creating community')); emit(const SpaceManagementError('Error creating community'));
@ -158,6 +191,54 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} }
} }
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( void _onSaveSpaces(
SaveSpacesEvent event, SaveSpacesEvent event,
Emitter<SpaceManagementState> emit, Emitter<SpaceManagementState> emit,
@ -166,17 +247,54 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
emit(SpaceManagementLoading()); emit(SpaceManagementLoading());
try { try {
final updatedSpaces = await saveSpacesHierarchically(event.spaces, event.communityUuid); final updatedSpaces =
await saveSpacesHierarchically(event.spaces, event.communityUuid);
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
emit(SpaceCreationSuccess(spaces: updatedSpaces)); emit(SpaceCreationSuccess(spaces: updatedSpaces));
if (previousState is SpaceManagementLoaded) {
_updateLoadedState(
previousState,
allSpaces,
event.communityUuid,
emit,
);
} else {
add(LoadCommunityAndSpacesEvent()); add(LoadCommunityAndSpacesEvent());
}
} catch (e) { } catch (e) {
emit(SpaceManagementError('Error saving spaces: $e')); emit(SpaceManagementError('Error saving spaces: $e'));
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded) {
emit(previousState); 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( Future<List<SpaceModel>> saveSpacesHierarchically(
List<SpaceModel> spaces, String communityUuid) async { List<SpaceModel> spaces, String communityUuid) async {
final orderedSpaces = flattenHierarchy(spaces); final orderedSpaces = flattenHierarchy(spaces);
@ -187,7 +305,6 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
for (var parent in parentsToDelete) { for (var parent in parentsToDelete) {
try { try {
// Ensure parent.uuid is not null before calling the API
if (parent.uuid != null) { if (parent.uuid != null) {
await _api.deleteSpace(communityUuid, parent.uuid!); await _api.deleteSpace(communityUuid, parent.uuid!);
} }

View File

@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; // Import for Offset 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 { abstract class SpaceManagementEvent extends Equatable {
const SpaceManagementEvent(); const SpaceManagementEvent();
@ -83,17 +84,6 @@ class CreateCommunityEvent extends SpaceManagementEvent {
List<Object> get props => [name, description]; List<Object> get props => [name, description];
} }
class FetchProductsEvent extends SpaceManagementEvent {}
class LoadSpaceHierarchyEvent extends SpaceManagementEvent {
final String communityId;
const LoadSpaceHierarchyEvent({required this.communityId});
@override
List<Object> get props => [communityId];
}
class UpdateCommunityEvent extends SpaceManagementEvent { class UpdateCommunityEvent extends SpaceManagementEvent {
final String communityUuid; final String communityUuid;
final String name; final String name;
@ -106,3 +96,47 @@ class UpdateCommunityEvent extends SpaceManagementEvent {
@override @override
List<Object> get props => [communityUuid, name]; 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

@ -1,7 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
abstract class SpaceManagementState extends Equatable { abstract class SpaceManagementState extends Equatable {
const SpaceManagementState(); const SpaceManagementState();
@ -17,10 +17,24 @@ class SpaceManagementLoading extends SpaceManagementState {}
class SpaceManagementLoaded extends SpaceManagementState { class SpaceManagementLoaded extends SpaceManagementState {
final List<CommunityModel> communities; final List<CommunityModel> communities;
final List<ProductModel> products; final List<ProductModel> products;
CommunityModel? selectedCommunity; // Include products in the state CommunityModel? selectedCommunity;
SpaceModel? selectedSpace;
SpaceManagementLoaded( SpaceManagementLoaded(
{required this.communities, required this.products, this.selectedCommunity}); {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 { class SpaceCreationSuccess extends SpaceManagementState {

View File

@ -1,5 +1,5 @@
import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
class CommunityModel { class CommunityModel {
final String uuid; final String uuid;

View File

@ -1,4 +1,4 @@
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
class Connection { class Connection {
final SpaceModel startSpace; final SpaceModel startSpace;

View File

@ -1,7 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/selected_product_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:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -44,7 +44,8 @@ class SpaceModel {
this.selectedProducts = const [], this.selectedProducts = const [],
}) : internalId = internalId ?? const Uuid().v4(); }) : internalId = internalId ?? const Uuid().v4();
factory SpaceModel.fromJson(Map<String, dynamic> json, {String? parentInternalId}) { factory SpaceModel.fromJson(Map<String, dynamic> json,
{String? parentInternalId}) {
final String internalId = json['internalId'] ?? const Uuid().v4(); final String internalId = json['internalId'] ?? const Uuid().v4();
final List<SpaceModel> children = json['children'] != null final List<SpaceModel> children = json['children'] != null
@ -56,7 +57,7 @@ class SpaceModel {
}).toList() }).toList()
: []; : [];
return SpaceModel( final instance = SpaceModel(
internalId: internalId, internalId: internalId,
uuid: json['uuid'] ?? '', uuid: json['uuid'] ?? '',
spaceTuyaUuid: json['spaceTuyaUuid'], spaceTuyaUuid: json['spaceTuyaUuid'],
@ -72,11 +73,14 @@ class SpaceModel {
isPrivate: json['parent']?['isPrivate'] ?? false, isPrivate: json['parent']?['isPrivate'] ?? false,
invitationCode: json['parent']?['invitationCode'], invitationCode: json['parent']?['invitationCode'],
children: [], children: [],
position: Offset(json['parent']?['x'] ?? 0, json['parent']?['y'] ?? 0), position:
Offset(json['parent']?['x'] ?? 0, json['parent']?['y'] ?? 0),
icon: json['parent']?['icon'] ?? Assets.location, icon: json['parent']?['icon'] ?? Assets.location,
) )
: null, : null,
community: json['community'] != null ? CommunityModel.fromJson(json['community']) : null, community: json['community'] != null
? CommunityModel.fromJson(json['community'])
: null,
children: children, children: children,
icon: json['icon'] ?? Assets.location, icon: json['icon'] ?? Assets.location,
position: Offset(json['x'] ?? 0, json['y'] ?? 0), position: Offset(json['x'] ?? 0, json['y'] ?? 0),
@ -90,6 +94,20 @@ class SpaceModel {
}).toList() }).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() { Map<String, dynamic> toMap() {

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

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/counter_widget.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/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';

View File

@ -1,32 +1,42 @@
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/spaces_management/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/create_community_dialog.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/color_manager.dart';
class BlankCommunityWidget extends StatelessWidget { class BlankCommunityWidget extends StatefulWidget {
const BlankCommunityWidget({Key? key}) : super(key: key); final List<CommunityModel> communities;
BlankCommunityWidget({required this.communities});
@override
_BlankCommunityWidgetState createState() => _BlankCommunityWidgetState();
}
class _BlankCommunityWidgetState extends State<BlankCommunityWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Container( child: Container(
color: ColorsManager.whiteColors, // Parent container with white background color:
ColorsManager.whiteColors, // Parent container with white background
child: GridView.builder( child: GridView.builder(
padding: const EdgeInsets.only(left: 40.0, top: 20.0), padding: const EdgeInsets.only(left: 40.0, top: 20.0),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400, // Each item's width will be 400 or less maxCrossAxisExtent: 400,
mainAxisSpacing: 10, // Spacing between items mainAxisSpacing: 10,
crossAxisSpacing: 10, // Spacing between items crossAxisSpacing: 10,
childAspectRatio: 2.0, // Aspect ratio for width:height (e.g., 300:150 = 2.0) childAspectRatio: 2.0,
), ),
itemCount: 1, // Only one item itemCount: 1, // Only one item
itemBuilder: (context, index) { itemBuilder: (context, index) {
return GestureDetector( return GestureDetector(
onTap: () => _showCreateCommunityDialog(context), onTap: () => _showCreateCommunityDialog(context),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, // Center align the content crossAxisAlignment:
CrossAxisAlignment.center, // Center align the content
children: [ children: [
Expanded( Expanded(
child: AspectRatio( child: AspectRatio(
@ -34,7 +44,7 @@ class BlankCommunityWidget extends StatelessWidget {
child: Container( child: Container(
decoration: ShapeDecoration( decoration: ShapeDecoration(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide( side: const BorderSide(
width: 4, width: 4,
strokeAlign: BorderSide.strokeAlignOutside, strokeAlign: BorderSide.strokeAlignOutside,
color: ColorsManager.borderColor, color: ColorsManager.borderColor,
@ -45,9 +55,8 @@ class BlankCommunityWidget extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(height: 9),
const SizedBox(height: 9), // Space between item and text Text('Blank',
Text('Blank', // Text below the item
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
)), )),
@ -63,6 +72,8 @@ class BlankCommunityWidget extends StatelessWidget {
showDialog( showDialog(
context: parentContext, context: parentContext,
builder: (context) => CreateCommunityDialog( builder: (context) => CreateCommunityDialog(
isEditMode: false,
existingCommunityNames: widget.communities.map((community) => community.name).toList(),
onCreateCommunity: (String communityName, String description) { onCreateCommunity: (String communityName, String description) {
parentContext.read<SpaceManagementBloc>().add( parentContext.read<SpaceManagementBloc>().add(
CreateCommunityEvent( CreateCommunityEvent(
@ -74,4 +85,5 @@ class BlankCommunityWidget extends StatelessWidget {
), ),
); );
} }
} }

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.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/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatelessWidget { class CommunityStructureHeader extends StatefulWidget {
final String? communityName; final String? communityName;
final bool isEditingName; final bool isEditingName;
final bool isSave; final bool isSave;
@ -13,9 +15,11 @@ class CommunityStructureHeader extends StatelessWidget {
final VoidCallback onDelete; final VoidCallback onDelete;
final VoidCallback onEditName; final VoidCallback onEditName;
final ValueChanged<String> onNameSubmitted; final ValueChanged<String> onNameSubmitted;
final List<CommunityModel> communities;
final CommunityModel? community;
const CommunityStructureHeader({ const CommunityStructureHeader(
Key? key, {super.key,
required this.communityName, required this.communityName,
required this.isSave, required this.isSave,
required this.isEditingName, required this.isEditingName,
@ -24,8 +28,15 @@ class CommunityStructureHeader extends StatelessWidget {
required this.onDelete, required this.onDelete,
required this.onEditName, required this.onEditName,
required this.onNameSubmitted, required this.onNameSubmitted,
}) : super(key: key); this.community,
required this.communities});
@override
State<CommunityStructureHeader> createState() =>
_CommunityStructureHeaderState();
}
class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -60,47 +71,63 @@ class CommunityStructureHeader extends StatelessWidget {
); );
} }
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) { Widget _buildCommunityInfo(ThemeData theme, double screenWidth) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Community Structure', 'Community Structure',
style: theme.textTheme.headlineLarge?.copyWith(color: ColorsManager.blackColor), style: theme.textTheme.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
), ),
if (communityName != null) if (widget.communityName != null)
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
if (!isEditingName) if (!widget.isEditingName)
Flexible( Flexible(
child: Text( child: Text(
communityName!, widget.communityName!,
style: style: theme.textTheme.bodyLarge
theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.blackColor), ?.copyWith(color: ColorsManager.blackColor),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
), ),
), ),
if (isEditingName) if (widget.isEditingName)
SizedBox( SizedBox(
width: screenWidth * 0.3, width: screenWidth * 0.1,
child: TextField( child: TextField(
controller: nameController, controller: widget.nameController,
decoration: const InputDecoration( decoration: const InputDecoration(
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
), ),
style: style: theme.textTheme.bodyLarge
theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.blackColor), ?.copyWith(color: ColorsManager.blackColor),
onSubmitted: onNameSubmitted, onSubmitted: widget.onNameSubmitted,
), ),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
GestureDetector( GestureDetector(
onTap: onEditName, onTap: () => _showCreateCommunityDialog(context),
child: SvgPicture.asset( child: SvgPicture.asset(
Assets.iconEdit, Assets.iconEdit,
width: 16, width: 16,
@ -110,7 +137,7 @@ class CommunityStructureHeader extends StatelessWidget {
], ],
), ),
), ),
if (isSave) ...[ if (widget.isSave) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
_buildActionButtons(theme), _buildActionButtons(theme),
], ],
@ -127,8 +154,9 @@ class CommunityStructureHeader extends StatelessWidget {
children: [ children: [
_buildButton( _buildButton(
label: "Save", label: "Save",
icon: const Icon(Icons.save, size: 18, color: ColorsManager.spaceColor), icon: const Icon(Icons.save,
onPressed: onSave, size: 18, color: ColorsManager.spaceColor),
onPressed: widget.onSave,
theme: theme), theme: theme),
], ],
); );
@ -159,7 +187,8 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible( Flexible(
child: Text( child: Text(
label, label,
style: theme.textTheme.bodySmall?.copyWith(color: ColorsManager.blackColor), style: theme.textTheme.bodySmall
?.copyWith(color: ColorsManager.blackColor),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
), ),

View File

@ -4,19 +4,19 @@ import 'package:flutter_bloc/flutter_bloc.dart';
// Syncrow project imports // Syncrow project imports
import 'package:syncrow_web/pages/common/buttons/add_space_button.dart'; import 'package:syncrow_web/pages/common/buttons/add_space_button.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/blank_community_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/community_structure_header_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/create_space_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/curved_line_painter.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_container_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class CommunityStructureArea extends StatefulWidget { class CommunityStructureArea extends StatefulWidget {
@ -24,12 +24,13 @@ class CommunityStructureArea extends StatefulWidget {
SpaceModel? selectedSpace; SpaceModel? selectedSpace;
final List<ProductModel>? products; final List<ProductModel>? products;
final ValueChanged<SpaceModel?>? onSpaceSelected; final ValueChanged<SpaceModel?>? onSpaceSelected;
final List<CommunityModel> communities;
final List<SpaceModel> spaces; final List<SpaceModel> spaces;
CommunityStructureArea({ CommunityStructureArea({
this.selectedCommunity, this.selectedCommunity,
this.selectedSpace, this.selectedSpace,
required this.communities,
this.products, this.products,
required this.spaces, required this.spaces,
this.onSpaceSelected, this.onSpaceSelected,
@ -52,7 +53,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void initState() { void initState() {
super.initState(); super.initState();
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces(); _adjustCanvasSizeForSpaces();
_nameController = TextEditingController( _nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '', text: widget.selectedCommunity?.name ?? '',
@ -79,12 +81,14 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
if (oldWidget.spaces != widget.spaces) { if (oldWidget.spaces != widget.spaces) {
setState(() { setState(() {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces(); _adjustCanvasSizeForSpaces();
}); });
} }
if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) { if (widget.selectedSpace != oldWidget.selectedSpace &&
widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_moveToSpace(widget.selectedSpace!); _moveToSpace(widget.selectedSpace!);
}); });
@ -94,14 +98,16 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.selectedCommunity == null) { if (widget.selectedCommunity == null) {
return BlankCommunityWidget(); return BlankCommunityWidget(
communities: widget.communities,
);
} }
Size screenSize = MediaQuery.of(context).size; Size screenSize = MediaQuery.of(context).size;
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
_deselectSpace(); _deselectSpace(context);
}, },
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
@ -113,7 +119,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CommunityStructureHeader( CommunityStructureHeader(
communities: widget.communities,
communityName: widget.selectedCommunity?.name, communityName: widget.selectedCommunity?.name,
community: widget.selectedCommunity,
isSave: isSave(spaces), isSave: isSave(spaces),
isEditingName: isEditingName, isEditingName: isEditingName,
nameController: _nameController, nameController: _nameController,
@ -156,9 +164,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
children: [ children: [
for (var connection in connections) for (var connection in connections)
Opacity( Opacity(
opacity: opacity: _isHighlightedConnection(connection)
_isHighlightedConnection(connection) ? 1.0 : 0.3, // Adjust opacity ? 1.0
child: CustomPaint(painter: CurvedLinePainter([connection])), : 0.3, // Adjust opacity
child: CustomPaint(
painter: CurvedLinePainter([connection])),
), ),
for (var entry in spaces.asMap().entries) for (var entry in spaces.asMap().entries)
if (entry.value.status != SpaceStatus.deleted) if (entry.value.status != SpaceStatus.deleted)
@ -167,10 +177,12 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
top: entry.value.position.dy, top: entry.value.position.dy,
child: SpaceCardWidget( child: SpaceCardWidget(
index: entry.key, index: entry.key,
onButtonTap: (int index, Offset newPosition, String direction) { onButtonTap: (int index, Offset newPosition,
String direction) {
_showCreateSpaceDialog( _showCreateSpaceDialog(
screenSize, screenSize,
position: spaces[index].position + newPosition, position:
spaces[index].position + newPosition,
parentIndex: index, parentIndex: index,
direction: direction, direction: direction,
); );
@ -183,7 +195,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_updateNodePosition(entry.value, newPosition); _updateNodePosition(entry.value, newPosition);
}, },
buildSpaceContainer: (int index) { buildSpaceContainer: (int index) {
final bool isHighlighted = _isHighlightedSpace(spaces[index]); final bool isHighlighted =
_isHighlightedSpace(spaces[index]);
return Opacity( return Opacity(
opacity: isHighlighted ? 1.0 : 0.3, opacity: isHighlighted ? 1.0 : 0.3,
@ -193,7 +206,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_showEditSpaceDialog(spaces[index]); _showEditSpaceDialog(spaces[index]);
}, },
onTap: () { onTap: () {
_selectSpace(spaces[index]); _selectSpace(context, spaces[index]);
}, },
icon: spaces[index].icon ?? '', icon: spaces[index].icon ?? '',
name: spaces[index].name, name: spaces[index].name,
@ -210,7 +223,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
child: AddSpaceButton( child: AddSpaceButton(
onTap: () { onTap: () {
_showCreateSpaceDialog(screenSize, _showCreateSpaceDialog(screenSize,
canvasHeight: canvasHeight, canvasWidth: canvasWidth); canvasHeight: canvasHeight,
canvasWidth: canvasWidth);
}, },
), ),
), ),
@ -271,11 +285,13 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return CreateSpaceDialog( return CreateSpaceDialog(
products: widget.products, products: widget.products,
parentSpace: parentIndex != null ? spaces[parentIndex] : null, parentSpace: parentIndex != null ? spaces[parentIndex] : null,
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts) { onCreateSpace: (String name, String icon,
List<SelectedProduct> selectedProducts) {
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 ?? _getCenterPosition(screenSize); Offset centerPosition =
position ?? _getCenterPosition(screenSize);
SpaceModel newSpace = SpaceModel( SpaceModel newSpace = SpaceModel(
name: name, name: name,
icon: icon, icon: icon,
@ -317,9 +333,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
products: widget.products, products: widget.products,
name: space.name, name: space.name,
icon: space.icon, icon: space.icon,
editSpace: space,
isEdit: true, isEdit: true,
selectedProducts: space.selectedProducts, selectedProducts: space.selectedProducts,
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts) { onCreateSpace: (String name, String icon,
List<SelectedProduct> selectedProducts) {
setState(() { setState(() {
// Update the space's properties // Update the space's properties
space.name = name; space.name = name;
@ -331,8 +349,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
}); });
}, },
// Pre-fill the dialog with current space data key: Key(space.name),
key: Key(space.name), // Add a unique key to ensure dialog refresh
); );
}, },
); );
@ -465,40 +482,30 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
..scale(1.2); ..scale(1.2);
} }
void _selectSpace(SpaceModel space) { void _selectSpace(BuildContext context, SpaceModel space) {
setState(() { context.read<SpaceManagementBloc>().add(
widget.selectedSpace = space; SelectSpaceEvent(
}); selectedCommunity: widget.selectedCommunity,
selectedSpace: space),
if (widget.onSpaceSelected != null) { );
widget.onSpaceSelected!(space);
}
} }
bool _isHighlightedSpace(SpaceModel space) { bool _isHighlightedSpace(SpaceModel space) {
if (widget.selectedSpace == null) return true; final selectedSpace = widget.selectedSpace;
if (space == widget.selectedSpace) return true; if (selectedSpace == null) return true;
if (widget.selectedSpace?.parent?.internalId == space.internalId) return true;
if (widget.selectedSpace?.children != null) { return space == selectedSpace ||
for (var child in widget.selectedSpace!.children) { selectedSpace.parent?.internalId == space.internalId ||
if (child.internalId == space.internalId) { selectedSpace.children
return true; ?.any((child) => child.internalId == space.internalId) ==
} true;
}
}
return false;
} }
void _deselectSpace() { void _deselectSpace(BuildContext context) {
if (widget.selectedSpace != null) { context.read<SpaceManagementBloc>().add(
setState(() { SelectSpaceEvent(
widget.selectedSpace = null; selectedCommunity: widget.selectedCommunity, selectedSpace: null),
}); );
if (widget.onSpaceSelected != null) {
widget.onSpaceSelected!(null); // Notify parent that no space is selected
}
}
} }
bool _isHighlightedConnection(Connection connection) { bool _isHighlightedConnection(Connection connection) {

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class CurvedLinePainter extends CustomPainter { class CurvedLinePainter extends CustomPainter {

View File

@ -2,24 +2,26 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/add_device_type_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/icon_selection_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/hoverable_button.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/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/space_icon_const.dart'; import 'package:syncrow_web/utils/constants/space_icon_const.dart';
class CreateSpaceDialog extends StatefulWidget { class CreateSpaceDialog extends StatefulWidget {
final Function(String, String, List<SelectedProduct> selectedProducts) onCreateSpace; final Function(String, String, List<SelectedProduct> selectedProducts)
onCreateSpace;
final List<ProductModel>? products; final List<ProductModel>? products;
final String? name; final String? name;
final String? icon; final String? icon;
final bool isEdit; final bool isEdit;
final List<SelectedProduct> selectedProducts; final List<SelectedProduct> selectedProducts;
final SpaceModel? parentSpace; final SpaceModel? parentSpace;
final SpaceModel? editSpace;
const CreateSpaceDialog( const CreateSpaceDialog(
{super.key, {super.key,
@ -29,6 +31,7 @@ class CreateSpaceDialog extends StatefulWidget {
this.name, this.name,
this.icon, this.icon,
this.isEdit = false, this.isEdit = false,
this.editSpace,
this.selectedProducts = const []}); this.selectedProducts = const []});
@override @override
@ -49,8 +52,10 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
super.initState(); super.initState();
selectedIcon = widget.icon ?? Assets.location; selectedIcon = widget.icon ?? Assets.location;
nameController = TextEditingController(text: widget.name ?? ''); nameController = TextEditingController(text: widget.name ?? '');
selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : []; selectedProducts =
isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty; widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
} }
@override @override
@ -59,7 +64,9 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog( return AlertDialog(
title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'), title: widget.isEdit
? const Text('Edit Space')
: const Text('Create New Space'),
backgroundColor: ColorsManager.whiteColors, backgroundColor: ColorsManager.whiteColors,
content: SizedBox( content: SizedBox(
width: screenWidth * 0.5, // Limit dialog width width: screenWidth * 0.5, // Limit dialog width
@ -126,11 +133,17 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
isNameFieldInvalid = value.isEmpty; isNameFieldInvalid = value.isEmpty;
if (!isNameFieldInvalid) { if (!isNameFieldInvalid) {
if (widget.parentSpace?.children if ((widget.parentSpace?.children.any(
.any((child) => child.name == value) ?? (child) => child.name == value) ??
false) { false) ||
(widget.parentSpace?.name == value) ||
(widget.editSpace?.children.any(
(child) => child.name == value) ??
false)) {
isNameFieldExist = true; isNameFieldExist = true;
isOkButtonEnabled = false;
} else { } else {
isNameFieldExist = false;
isOkButtonEnabled = true; isOkButtonEnabled = true;
} }
} }
@ -218,7 +231,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
padding: const EdgeInsets.only(left: 6.0), padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset( child: SvgPicture.asset(
Assets.addIcon, Assets.addIcon,
width: screenWidth * 0.015, // Adjust icon size width: screenWidth *
0.015, // Adjust icon size
height: screenWidth * 0.015, height: screenWidth * 0.015,
), ),
), ),
@ -226,8 +240,11 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
Flexible( Flexible(
child: Text( child: Text(
'Add devices / Assign a space model', 'Add devices / Assign a space model',
overflow: TextOverflow.ellipsis, // Prevent overflow overflow: TextOverflow
style: Theme.of(context).textTheme.bodyMedium, .ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
), ),
), ),
], ],
@ -262,16 +279,20 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
}); });
return; return;
} else { } else {
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? ''); String newName = enteredName.isNotEmpty
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) { if (newName.isNotEmpty) {
widget.onCreateSpace(newName, selectedIcon, selectedProducts); widget.onCreateSpace(
newName, selectedIcon, selectedProducts);
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
}, },
borderRadius: 10, borderRadius: 10,
backgroundColor: backgroundColor: isOkButtonEnabled
isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor, ? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors, foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'), child: const Text('OK'),
), ),
@ -318,7 +339,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
children: [ children: [
for (var i = 0; i < selectedProducts.length; i++) ...[ for (var i = 0; i < selectedProducts.length; i++) ...[
HoverableButton( HoverableButton(
iconPath: _mapIconToProduct(selectedProducts[i].productId, products), iconPath:
_mapIconToProduct(selectedProducts[i].productId, products),
text: 'x${selectedProducts[i].count}', text: 'x${selectedProducts[i].count}',
onTap: () { onTap: () {
setState(() { setState(() {
@ -328,7 +350,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
}, },
), ),
if (i < selectedProducts.length - 1) if (i < selectedProducts.length - 1)
const SizedBox(width: 2), // Add space except after the last button const SizedBox(
width: 2), // Add space except after the last button
], ],
const SizedBox(width: 2), const SizedBox(width: 2),
GestureDetector( GestureDetector(
@ -364,6 +387,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
); );
} }
String _mapIconToProduct(String uuid, List<ProductModel> products) { String _mapIconToProduct(String uuid, List<ProductModel> products) {
// Find the product with the matching UUID // Find the product with the matching UUID
final product = products.firstWhere( final product = products.firstWhere(

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

@ -2,30 +2,22 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/search_bar.dart'; import 'package:syncrow_web/common/search_bar.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/community_tile.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/dialogs/create_community_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_tile_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
class SidebarWidget extends StatefulWidget { class SidebarWidget extends StatefulWidget {
final Function(CommunityModel)? onCommunitySelected;
final Function(SpaceModel?)? onSpaceSelected;
final List<CommunityModel> communities; final List<CommunityModel> communities;
final Function(String?)? onSelectedSpaceChanged; // New callback
final String? selectedSpaceUuid; final String? selectedSpaceUuid;
const SidebarWidget({ const SidebarWidget({
super.key, super.key,
this.onCommunitySelected,
this.onSpaceSelected,
this.onSelectedSpaceChanged,
required this.communities, required this.communities,
this.selectedSpaceUuid, this.selectedSpaceUuid,
}); });
@ -42,7 +34,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedId = widget.selectedSpaceUuid; // Initialize with the passed selected space UUID _selectedId = widget
.selectedSpaceUuid; // Initialize with the passed selected space UUID
} }
@override @override
@ -55,22 +48,6 @@ class _SidebarWidgetState extends State<SidebarWidget> {
} }
} }
void _showCreateCommunityDialog(BuildContext parentContext) {
showDialog(
context: parentContext,
builder: (context) => CreateCommunityDialog(
onCreateCommunity: (String communityName, String description) {
parentContext.read<SpaceManagementBloc>().add(
CreateCommunityEvent(
name: communityName,
description: description,
),
);
},
),
);
}
// Function to filter communities based on the search query // Function to filter communities based on the search query
List<CommunityModel> _filterCommunities() { List<CommunityModel> _filterCommunities() {
if (_searchQuery.isEmpty) { if (_searchQuery.isEmpty) {
@ -83,8 +60,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
return widget.communities.where((community) { return widget.communities.where((community) {
final containsQueryInCommunity = final containsQueryInCommunity =
community.name.toLowerCase().contains(_searchQuery.toLowerCase()); community.name.toLowerCase().contains(_searchQuery.toLowerCase());
final containsQueryInSpaces = final containsQueryInSpaces = community.spaces
community.spaces.any((space) => _containsQuery(space, _searchQuery.toLowerCase())); .any((space) => _containsQuery(space, _searchQuery.toLowerCase()));
return containsQueryInCommunity || containsQueryInSpaces; return containsQueryInCommunity || containsQueryInSpaces;
}).toList(); }).toList();
@ -93,8 +70,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
// Helper function to determine if any space or its children match the search query // Helper function to determine if any space or its children match the search query
bool _containsQuery(SpaceModel space, String query) { bool _containsQuery(SpaceModel space, String query) {
final matchesSpace = space.name.toLowerCase().contains(query); final matchesSpace = space.name.toLowerCase().contains(query);
final matchesChildren = final matchesChildren = space.children.any((child) =>
space.children.any((child) => _containsQuery(child, query)); // Recursive check for children _containsQuery(child, query)); // Recursive check for children
// If the space or any of its children match the query, expand this space // If the space or any of its children match the query, expand this space
if (matchesSpace || matchesChildren) { if (matchesSpace || matchesChildren) {
@ -128,7 +105,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
width: 300, width: 300,
decoration: subSectionContainerDecoration, decoration: subSectionContainerDecoration,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, // Ensures the Column only takes necessary height mainAxisSize:
MainAxisSize.min, // Ensures the Column only takes necessary height
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Communities title with the add button // Communities title with the add button
@ -143,7 +121,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
color: Colors.black, color: Colors.black,
)), )),
GestureDetector( GestureDetector(
onTap: () => _showCreateCommunityDialog(context), onTap: () => _navigateToBlank(context),
child: Container( child: Container(
width: 30, width: 30,
height: 30, height: 30,
@ -176,7 +154,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
Expanded( Expanded(
child: ListView( child: ListView(
children: filteredCommunities.map((community) { children: filteredCommunities.map((community) {
return _buildCommunityTile(community); return _buildCommunityTile(context, community);
}).toList(), }).toList(),
), ),
), ),
@ -185,7 +163,16 @@ class _SidebarWidgetState extends State<SidebarWidget> {
); );
} }
Widget _buildCommunityTile(CommunityModel community) { 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; bool hasChildren = community.spaces.isNotEmpty;
return CommunityTile( return CommunityTile(
@ -199,23 +186,23 @@ class _SidebarWidgetState extends State<SidebarWidget> {
_selectedSpaceUuid = null; // Update the selected community _selectedSpaceUuid = null; // Update the selected community
}); });
if (widget.onCommunitySelected != null) { context.read<SpaceManagementBloc>().add(
widget.onCommunitySelected!(community); SelectCommunityEvent(selectedCommunity: community),
widget.onSpaceSelected!(null); // Pass the entire community );
}
}, },
onExpansionChanged: (String title, bool expanded) { onExpansionChanged: (String title, bool expanded) {
_handleExpansionChange(community.uuid, expanded); _handleExpansionChange(community.uuid, expanded);
}, },
children: hasChildren children: hasChildren
? community.spaces.map((space) => _buildSpaceTile(space, community)).toList() ? community.spaces
.map((space) => _buildSpaceTile(space, community))
.toList()
: null, // Render spaces within the community : null, // Render spaces within the community
); );
} }
Widget _buildSpaceTile(SpaceModel space, CommunityModel community) { Widget _buildSpaceTile(SpaceModel space, CommunityModel community) {
bool isExpandedSpace = _isSpaceOrChildSelected(space); bool isExpandedSpace = _isSpaceOrChildSelected(space);
// Check if space should be expanded
return SpaceTile( return SpaceTile(
title: space.name, title: space.name,
key: ValueKey(space.uuid), key: ValueKey(space.uuid),
@ -230,17 +217,15 @@ class _SidebarWidgetState extends State<SidebarWidget> {
_selectedSpaceUuid = space.uuid; _selectedSpaceUuid = space.uuid;
}); });
if (widget.onSpaceSelected != null) { context.read<SpaceManagementBloc>().add(
widget.onCommunitySelected!(community); SelectSpaceEvent(
widget.onSpaceSelected!(space); selectedCommunity: community, selectedSpace: space),
} );
if (widget.onSelectedSpaceChanged != null) {
widget.onSelectedSpaceChanged!(space.uuid);
}
}, },
children: space.children.isNotEmpty children: space.children.isNotEmpty
? space.children.map((childSpace) => _buildSpaceTile(childSpace, community)).toList() ? space.children
.map((childSpace) => _buildSpaceTile(childSpace, community))
.toList()
: [], // Recursively render child spaces if available : [], // Recursively render child spaces if available
); );
} }

View File

@ -0,0 +1,21 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'community_dialog_event.dart';
import 'community_dialog_state.dart';
class CommunityDialogBloc extends Bloc<CommunityDialogEvent, CommunityDialogState> {
final List<String> existingCommunityNames;
CommunityDialogBloc(this.existingCommunityNames)
: super(CommunityDialogInitial()) {
on<ValidateCommunityNameEvent>((event, emit) {
final trimmedName = event.name.trim();
final isNameValid = !existingCommunityNames.contains(trimmedName);
final isNameEmpty = trimmedName.isEmpty;
emit(CommunityNameValidationState(
isNameValid: isNameValid,
isNameEmpty: isNameEmpty,
));
});
}
}

View File

@ -0,0 +1,15 @@
import 'package:equatable/equatable.dart';
abstract class CommunityDialogEvent extends Equatable {
@override
List<Object?> get props => [];
}
class ValidateCommunityNameEvent extends CommunityDialogEvent {
final String name;
ValidateCommunityNameEvent(this.name);
@override
List<Object?> get props => [name];
}

View File

@ -0,0 +1,21 @@
import 'package:equatable/equatable.dart';
abstract class CommunityDialogState extends Equatable {
@override
List<Object?> get props => [];
}
class CommunityDialogInitial extends CommunityDialogState {}
class CommunityNameValidationState extends CommunityDialogState {
final bool isNameValid;
final bool isNameEmpty;
CommunityNameValidationState({
required this.isNameValid,
required this.isNameEmpty,
});
@override
List<Object?> get props => [isNameValid, isNameEmpty];
}

View File

@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
class CreateCommunityDialog extends StatelessWidget {
final Function(String name, String description) onCreateCommunity;
final List<String> existingCommunityNames;
final bool isEditMode;
final String? initialName;
const CreateCommunityDialog({
super.key,
required this.onCreateCommunity,
required this.existingCommunityNames,
required this.isEditMode,
this.initialName,
});
@override
Widget build(BuildContext context) {
final nameController =
TextEditingController(text: isEditMode ? initialName : '');
return BlocProvider(
create: (_) => CommunityDialogBloc(existingCommunityNames),
child: Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: ColorsManager.transparentColor,
child: Stack(
children: [
Container(
width: MediaQuery.of(context).size.width * 0.3,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.25),
blurRadius: 20,
spreadRadius: 5,
offset: const Offset(0, 5),
),
],
),
child: SingleChildScrollView(
child: BlocBuilder<CommunityDialogBloc, CommunityDialogState>(
builder: (context, state) {
bool isNameValid = true;
bool isNameEmpty = false;
if (state is CommunityNameValidationState) {
isNameValid = state.isNameValid;
isNameEmpty = state.isNameEmpty;
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isEditMode ? 'Edit Community Name' : 'Community Name',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 18),
TextField(
controller: nameController,
onChanged: (value) {
context
.read<CommunityDialogBloc>()
.add(ValidateCommunityNameEvent(value));
},
style: const TextStyle(
color: ColorsManager.blackColor,
),
decoration: InputDecoration(
hintText: 'Please enter the community name',
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isNameValid && !isNameEmpty
? ColorsManager.boxColor
: ColorsManager.red,
width: 1,
),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
width: 1.5,
),
),
),
),
if (!isNameValid)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name already exists.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
if (isNameEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name should not be empty.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
),
),
const SizedBox(width: 16),
Expanded(
child: DefaultButton(
onPressed: () {
if (isNameValid && !isNameEmpty) {
onCreateCommunity(
nameController.text.trim(),
"",
);
Navigator.of(context).pop();
}
},
backgroundColor: isNameValid && !isNameEmpty
? ColorsManager.secondaryColor
: ColorsManager.lightGrayColor,
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
],
),
],
);
},
),
),
),
],
),
),
);
}
}

View File

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_state.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/loaded_space_widget.dart';
import 'package:syncrow_web/services/product_api.dart';
import 'package:syncrow_web/services/space_mana_api.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class SpaceManagementPage extends StatefulWidget {
const SpaceManagementPage({super.key});
@override
SpaceManagementPageState createState() => SpaceManagementPageState();
}
class SpaceManagementPageState extends State<SpaceManagementPage> {
CommunityModel? selectedCommunity;
SpaceModel? selectedSpace;
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 SpaceManagementLoaded) {
int selectedIndex = state.communities.indexWhere(
(community) => community.uuid == selectedCommunity?.uuid,
);
if (selectedIndex != -1) {
selectedCommunity = state.communities[selectedIndex];
} else if (state.selectedCommunity != null) {
selectedCommunity = state.selectedCommunity;
} else {
selectedCommunity = null;
selectedSpace = null;
}
return LoadedSpaceView(
communities: state.communities,
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
products: state.products,
onCommunitySelected: (community) {
setState(() {
selectedCommunity = community;
});
},
onSpaceSelected: (space) {
setState(() {
selectedSpace = space;
});
},
);
} else if (state is SpaceManagementError) {
return Center(child: Text('Error: ${state.errorMessage}'));
}
return Container();
}),
),
);
}
}

View File

@ -1,137 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateCommunityDialog extends StatefulWidget {
final Function(String name, String description) onCreateCommunity;
const CreateCommunityDialog({super.key, required this.onCreateCommunity});
@override
CreateCommunityDialogState createState() => CreateCommunityDialogState();
}
class CreateCommunityDialogState extends State<CreateCommunityDialog> {
String enteredName = ''; // Store the entered community name
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: Colors.transparent, // Transparent for shadow effect
child: Stack(
children: [
// Background container with shadow and rounded corners
Container(
width: 490,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 20,
spreadRadius: 5,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Community Name',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Input field for the community name
TextField(
onChanged: (value) {
enteredName = value; // Capture entered name
},
style: const TextStyle(
color: Colors.black,
),
decoration: InputDecoration(
hintText: 'Please enter the community name',
filled: true,
fillColor: ColorsManager.boxColor,
hintStyle: const TextStyle(
fontSize: 14,
color: ColorsManager.grayBorder,
fontWeight: FontWeight.w400,
),
border: OutlineInputBorder(
borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1),
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1),
),
),
),
const SizedBox(height: 24),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: ColorsManager.boxColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'Cancel',
style: TextStyle(color: Colors.black),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () {
if (enteredName.isNotEmpty) {
widget.onCreateCommunity(
enteredName,
"",
);
Navigator.of(context).pop();
}
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('OK'),
),
),
],
),
],
),
),
],
),
);
}
}

View File

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

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/services/api/http_service.dart'; 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';

View File

@ -36,7 +36,6 @@ class SceneApi {
static Future<Map<String, dynamic>> createAutomation( static Future<Map<String, dynamic>> createAutomation(
CreateAutomationModel createAutomationModel) async { CreateAutomationModel createAutomationModel) async {
try { try {
debugPrint("automation body ${createAutomationModel.toMap()}");
final response = await _httpService.post( final response = await _httpService.post(
path: ApiEndpoints.createAutomation, path: ApiEndpoints.createAutomation,
body: createAutomationModel.toMap(), body: createAutomationModel.toMap(),
@ -138,29 +137,49 @@ class SceneApi {
path: ApiEndpoints.getAutomationDetails path: ApiEndpoints.getAutomationDetails
.replaceAll('{automationId}', automationId), .replaceAll('{automationId}', automationId),
showServerMessage: false, showServerMessage: false,
expectedResponseModel: (json) => RoutineDetailsModel.fromJson(json), expectedResponseModel: (json) => RoutineDetailsModel.fromMap(json),
);
return response;
} catch (e) {
rethrow;
}
}
//update Scene
static updateScene(CreateSceneModel createSceneModel, String sceneId) async {
try {
final response = await _httpService.put(
path: ApiEndpoints.updateScene.replaceAll('{sceneId}', sceneId),
body: createSceneModel
.toJson(sceneId.isNotEmpty == true ? sceneId : null),
expectedResponseModel: (json) {
return json;
},
);
return response;
} catch (e) {
rethrow;
}
}
//update automation
static updateAutomation(
CreateAutomationModel createAutomationModel, String automationId) async {
try {
final response = await _httpService.put(
path: ApiEndpoints.updateAutomation
.replaceAll('{automationId}', automationId),
body: createAutomationModel
.toJson(automationId.isNotEmpty == true ? automationId : null),
expectedResponseModel: (json) {
return json;
},
); );
return response; return response;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
//
// //updateAutomationStatus
// static Future<bool> updateAutomationStatus(String automationId,
// AutomationStatusUpdate createAutomationEnable) async {
// try {
// final response = await _httpService.put(
// path: ApiEndpoints.updateAutomationStatus
// .replaceAll('{automationId}', automationId),
// body: createAutomationEnable.toMap(),
// expectedResponseModel: (json) => json['success'],
// );
// return response;
// } catch (e) {
// rethrow;
// }
// }
//getScene //getScene
static Future<RoutineDetailsModel> getSceneDetails(String sceneId) async { static Future<RoutineDetailsModel> getSceneDetails(String sceneId) async {
@ -168,51 +187,14 @@ class SceneApi {
final response = await _httpService.get( final response = await _httpService.get(
path: ApiEndpoints.getScene.replaceAll('{sceneId}', sceneId), path: ApiEndpoints.getScene.replaceAll('{sceneId}', sceneId),
showServerMessage: false, showServerMessage: false,
expectedResponseModel: (json) => RoutineDetailsModel.fromJson(json), expectedResponseModel: (json) =>
RoutineDetailsModel.fromMap(json['data']),
); );
return response; return response;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
//
// //update Scene
// static updateScene(CreateSceneModel createSceneModel, String sceneId) async {
// try {
// final response = await _httpService.put(
// path: ApiEndpoints.updateScene.replaceAll('{sceneId}', sceneId),
// body: createSceneModel
// .toJson(sceneId.isNotEmpty == true ? sceneId : null),
// expectedResponseModel: (json) {
// return json;
// },
// );
// return response;
// } catch (e) {
// rethrow;
// }
// }
//
// //update automation
// static updateAutomation(
// CreateAutomationModel createAutomationModel, String automationId) async {
// try {
// final response = await _httpService.put(
// path: ApiEndpoints.updateAutomation
// .replaceAll('{automationId}', automationId),
// body: createAutomationModel
// .toJson(automationId.isNotEmpty == true ? automationId : null),
// expectedResponseModel: (json) {
// return json;
// },
// );
// return response;
// } catch (e) {
// rethrow;
// }
// }
//
//delete Scene //delete Scene
static Future<bool> deleteScene( static Future<bool> deleteScene(

View File

@ -1,29 +1,38 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_response_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_model.dart';
import 'package:syncrow_web/services/api/http_service.dart'; 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 CommunitySpaceManagementApi { class CommunitySpaceManagementApi {
// Community Management APIs // Community Management APIs
Future<List<CommunityModel>> fetchCommunities() async { Future<List<CommunityModel>> fetchCommunities({int page = 1}) async {
try { try {
final response = await HTTPService().get( List<CommunityModel> allCommunities = [];
path: ApiEndpoints.getCommunityList, bool hasNext = true;
expectedResponseModel: (json) {
// Access the 'data' key from the response
List<dynamic> jsonData = json['data'];
// Check if jsonData is actually a List while (hasNext) {
await HTTPService().get(
path: ApiEndpoints.getCommunityList,
queryParameters: {'page': page},
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
hasNext = json['hasNext'] ?? false;
int currentPage = json['page'] ?? 1;
List<CommunityModel> communityList = jsonData.map((jsonItem) { List<CommunityModel> communityList = jsonData.map((jsonItem) {
return CommunityModel.fromJson(jsonItem); return CommunityModel.fromJson(jsonItem);
}).toList(); }).toList();
allCommunities.addAll(communityList);
page = currentPage + 1;
return communityList; return communityList;
}, },
); );
return response; }
return allCommunities;
} catch (e) { } catch (e) {
debugPrint('Error fetching communities: $e'); debugPrint('Error fetching communities: $e');
return []; return [];
@ -33,7 +42,8 @@ class CommunitySpaceManagementApi {
Future<CommunityModel?> getCommunityById(String communityId) async { Future<CommunityModel?> getCommunityById(String communityId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), path: ApiEndpoints.getCommunityById
.replaceAll('{communityId}', communityId),
expectedResponseModel: (json) { expectedResponseModel: (json) {
return CommunityModel.fromJson(json['data']); return CommunityModel.fromJson(json['data']);
}, },
@ -45,7 +55,8 @@ class CommunitySpaceManagementApi {
} }
} }
Future<CommunityModel?> createCommunity(String name, String description) async { Future<CommunityModel?> createCommunity(
String name, String description) async {
try { try {
final response = await HTTPService().post( final response = await HTTPService().post(
path: ApiEndpoints.createCommunity, path: ApiEndpoints.createCommunity,
@ -67,7 +78,8 @@ class CommunitySpaceManagementApi {
Future<bool> updateCommunity(String communityId, String name) async { Future<bool> updateCommunity(String communityId, String name) async {
try { try {
final response = await HTTPService().put( final response = await HTTPService().put(
path: ApiEndpoints.updateCommunity.replaceAll('{communityId}', communityId), path: ApiEndpoints.updateCommunity
.replaceAll('{communityId}', communityId),
body: { body: {
'name': name, 'name': name,
}, },
@ -85,7 +97,8 @@ class CommunitySpaceManagementApi {
Future<bool> deleteCommunity(String communityId) async { Future<bool> deleteCommunity(String communityId) async {
try { try {
final response = await HTTPService().delete( final response = await HTTPService().delete(
path: ApiEndpoints.deleteCommunity.replaceAll('{communityId}', communityId), path: ApiEndpoints.deleteCommunity
.replaceAll('{communityId}', communityId),
expectedResponseModel: (json) { expectedResponseModel: (json) {
return json['success'] ?? false; return json['success'] ?? false;
}, },
@ -236,10 +249,12 @@ class CommunitySpaceManagementApi {
Future<List<SpaceModel>> getSpaceHierarchy(String communityId) async { Future<List<SpaceModel>> getSpaceHierarchy(String communityId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getSpaceHierarchy.replaceAll('{communityId}', communityId), path: ApiEndpoints.getSpaceHierarchy
.replaceAll('{communityId}', communityId),
expectedResponseModel: (json) { expectedResponseModel: (json) {
final spaceModels = final spaceModels = (json['data'] as List)
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); .map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
return spaceModels; return spaceModels;
}, },

View File

@ -3,7 +3,7 @@ import 'package:syncrow_web/pages/access_management/view/access_management.dart'
import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/auth/view/login_page.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart';
import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart';
import 'package:syncrow_web/pages/spaces_management/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart';
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart';
@ -32,7 +32,7 @@ class AppRoutes {
), ),
GoRoute( GoRoute(
path: RoutesConst.spacesManagementPage, path: RoutesConst.spacesManagementPage,
builder: (context, state) => SpaceManagementPage()), builder: (context, state) => const SpaceManagementPage()),
]; ];
} }
} }

View File

@ -74,4 +74,7 @@ abstract class ApiEndpoints {
static const String deleteScene = '/scene/tap-to-run/{sceneId}'; static const String deleteScene = '/scene/tap-to-run/{sceneId}';
static const String deleteAutomation = '/automation/{automationId}'; static const String deleteAutomation = '/automation/{automationId}';
static const String updateScene = '/scene/tap-to-run/{sceneId}';
static const String updateAutomation = '/automation/{automationId}';
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/navigation_service.dart'; import 'package:syncrow_web/utils/navigation_service.dart';
class CustomSnackBar { class CustomSnackBar {
@ -11,6 +12,35 @@ class CustomSnackBar {
} }
} }
static redSnackBar(String message) {
final key = NavigationService.snackbarKey;
BuildContext? currentContext = key?.currentContext;
if (key != null && currentContext != null) {
final snackBar = SnackBar(
padding: const EdgeInsets.all(16),
backgroundColor: ColorsManager.red,
content: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
const Icon(
Icons.check_circle,
color: ColorsManager.whiteColors,
size: 32,
),
const SizedBox(
width: 8,
),
Text(
message,
style: Theme.of(currentContext)
.textTheme
.bodySmall!
.copyWith(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green),
)
]),
);
key.currentState?.showSnackBar(snackBar);
}
}
static greenSnackBar(String message) { static greenSnackBar(String message) {
final key = NavigationService.snackbarKey; final key = NavigationService.snackbarKey;
BuildContext? currentContext = key?.currentContext; BuildContext? currentContext = key?.currentContext;
@ -29,8 +59,10 @@ class CustomSnackBar {
), ),
Text( Text(
message, message,
style: Theme.of(currentContext).textTheme.bodySmall!.copyWith( style: Theme.of(currentContext)
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green), .textTheme
.bodySmall!
.copyWith(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green),
) )
]), ]),
); );