Sp 1601 fe community and space dialog redesign in the routine tab (#236)

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1601](https://syncrow.atlassian.net/browse/SP-1601)

## Description

<!--- Describe your changes in detail -->
redesign community and space dialog and On Access management and Devices
Management Pages Clicking the Reset button should reset the table and
buttons not refresh the Page

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [x]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1601]:
https://syncrow.atlassian.net/browse/SP-1601?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
mohammadnemer1
2025-06-11 14:46:35 +03:00
committed by GitHub
13 changed files with 416 additions and 191 deletions

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -267,7 +267,8 @@ class AccessBloc extends Bloc<AccessEvent, AccessState> {
selectedIndex = 0;
effectiveTimeTimeStamp = null;
expirationTimeTimeStamp = null;
add(FetchTableData());
filteredData = List.from(data);
emit(TableLoaded(filteredData));
}
String timestampToDate(dynamic timestamp) {

View File

@ -34,7 +34,8 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
runSpacing: 10,
children: [
_buildSearchField("Space Name", _unitNameController, 200),
_buildSearchField("Device Name / Product Name", _productNameController, 300),
_buildSearchField(
"Device Name / Product Name", _productNameController, 300),
_buildSearchResetButtons(),
],
);
@ -74,9 +75,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
onReset: () {
_unitNameController.clear();
_productNameController.clear();
context.read<DeviceManagementBloc>()
..add(ResetFilters())
..add(FetchDevices(context));
context.read<DeviceManagementBloc>().add(ResetFilters());
},
);
}

View File

@ -379,7 +379,7 @@ class GarageDoorBloc extends Bloc<GarageDoorEvent, GarageDoorState> {
}
emit(GarageDoorLoadedState(status: deviceStatus));
add(GarageDoorControlEvent(
deviceId: event.deviceId,
deviceId: deviceId,
value: deviceStatus.delay.inSeconds,
code: 'countdown_1'));
} catch (e) {
@ -396,7 +396,7 @@ class GarageDoorBloc extends Bloc<GarageDoorEvent, GarageDoorState> {
_updateLocalValue(event.code, event.value);
emit(GarageDoorLoadedState(status: deviceStatus));
final success = await _runDeBouncer(
deviceId: event.deviceId,
deviceId: deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,

View File

@ -36,7 +36,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
if (user != null && user!.project != null) {
await ProjectManager.setProjectUUID(user!.project!.uuid);
// NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent());
}
add(FetchTermEvent());
add(FetchPolicyEvent());

View File

@ -20,12 +20,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
// Flag to track whether the dialog is already shown.
bool _dialogShown = false;
@override
void initState() {
super.initState();
final homeBloc = BlocProvider.of<HomeBloc>(context);
// homeBloc.add(const FetchUserInfo());
}
@override
Widget build(BuildContext context) {

View File

@ -1,29 +1,73 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/create_new_routines/dropdown_menu_content.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'space_tree_dropdown_bloc.dart';
class CommunityDropdown extends StatelessWidget {
final String? selectedValue;
final List<CommunityModel> communities;
final Function(String?) onChanged;
final TextEditingController _searchController = TextEditingController();
class SpaceTreeDropdown extends StatefulWidget {
final String? selectedSpaceId;
final Function(String?)? onChanged;
CommunityDropdown({
Key? key,
required this.selectedValue,
required this.onChanged,
required this.communities,
}) : super(key: key);
const SpaceTreeDropdown({
super.key,
this.selectedSpaceId,
this.onChanged,
});
@override
State<SpaceTreeDropdown> createState() => _SpaceTreeDropdownState();
}
class _SpaceTreeDropdownState extends State<SpaceTreeDropdown> {
late SpaceTreeDropdownBloc _dropdownBloc;
final LayerLink _layerLink = LayerLink();
OverlayEntry? _overlayEntry;
@override
void initState() {
super.initState();
_dropdownBloc = SpaceTreeDropdownBloc(widget.selectedSpaceId);
}
@override
void dispose() {
_dropdownBloc.close();
_removeOverlay();
super.dispose();
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
return BlocProvider.value(
value: _dropdownBloc,
child: BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, spaceTreeState) {
final communities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
return BlocBuilder<SpaceTreeDropdownBloc, SpaceTreeDropdownState>(
builder: (context, dropdownState) {
final selectedCommunity = _findCommunity(
communities,
dropdownState.selectedSpaceId,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
"Community",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w400,
@ -31,126 +75,107 @@ class CommunityDropdown extends StatelessWidget {
color: ColorsManager.blackColor,
),
),
const SizedBox(height: 8),
SizedBox(
),
CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onTap: () => _toggleDropdown(context, communities),
child: Container(
height: 46,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
underline: const SizedBox(),
value: selectedValue,
items: communities.map((community) {
return DropdownMenuItem<String>(
value: community.uuid,
child: Text(
' ${community.name}',
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}).toList(),
onChanged: onChanged,
style: const TextStyle(color: Colors.black),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
" Please Select",
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
),
),
customButton: Container(
height: 45,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10),
child: Text(
selectedValue != null
? " ${communities.firstWhere((element) => element.uuid == selectedValue).name}"
: ' Please Select',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: selectedValue != null
? Colors.black
selectedCommunity?.name ?? 'Please Select',
style: TextStyle(
color: selectedCommunity != null
? ColorsManager.blackColor
: ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w400,
fontSize: 13,
),
),
Expanded(
child: Container(
),
Container(
decoration: BoxDecoration(
color: Colors.grey[100],
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
width: 33,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
),
dropdownSearchData: DropdownSearchData(
searchController: _searchController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TextFormField(
style: const TextStyle(color: Colors.black),
controller: _searchController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 12,
),
hintText: 'Search for community...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
searchMatchFn: (item, searchValue) {
final communityName =
(item.child as Text).data?.toLowerCase() ?? '';
return communityName
.contains(searchValue.toLowerCase().trim());
},
),
onMenuStateChange: (isOpen) {
if (!isOpen) {
_searchController.clear();
}
},
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
))
],
);
},
);
},
),
);
}
void _toggleDropdown(BuildContext context, List<CommunityModel> communities) {
if (_overlayEntry != null) {
_removeOverlay();
return;
}
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
width: 300,
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: const Offset(0, 48),
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
child: DropdownMenuContent(
selectedSpaceId: _dropdownBloc.state.selectedSpaceId,
onChanged: (id) {
if (id != null && mounted) {
_dropdownBloc.add(SpaceTreeDropdownSelectEvent(id));
widget.onChanged?.call(id);
_removeOverlay();
}
},
onClose: _removeOverlay,
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
CommunityModel? _findCommunity(
List<CommunityModel> communities, String? communityId) {
if (communityId == null) return null;
try {
return communities.firstWhere((c) => c.uuid == communityId);
} catch (e) {
return null;
}
}
}

View File

@ -18,6 +18,7 @@ class CreateNewRoutinesDialog extends StatefulWidget {
class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
String? _selectedCommunity;
String? _selectedSpace;
String? _selectedId;
@override
Widget build(BuildContext context) {
@ -30,7 +31,7 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
final spaces = _bloc.spacesOnlyWithDevices;
final isLoadingCommunities = state is CommunitiesLoadingState;
final isLoadingSpaces = state is SpaceWithDeviceLoadingState;
String spaceHint = 'Select a community first';
String spaceHint = 'Please Select';
if (_selectedCommunity != null) {
if (isLoadingSpaces) {
spaceHint = 'Loading spaces...';
@ -40,7 +41,10 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
spaceHint = 'Select Space';
}
}
if (_selectedId != null && _selectedCommunity != _selectedId) {
_selectedSpace = null;
_selectedCommunity = _selectedId;
}
return AlertDialog(
backgroundColor: Colors.white,
insetPadding: EdgeInsets.zero,
@ -51,7 +55,9 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
'Create New Routines',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.primaryColor,
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
content: Stack(
@ -60,27 +66,28 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: CommunityDropdown(
communities: _bloc.communities..sort(
(a, b) => a.name.toLowerCase().compareTo(
b.name.toLowerCase(),
),
),
selectedValue: _selectedCommunity,
padding:
const EdgeInsets.only(left: 13, right: 8),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
onChanged: (String? newValue) {
setState(() {
_selectedCommunity = newValue;
_selectedSpace = null;
});
if (newValue != null) {
_bloc.add(SpaceOnlyWithDevicesEvent(newValue));
setState(() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(SpaceOnlyWithDevicesEvent(
_selectedId!));
}
},
),
),
],
)),
const SizedBox(height: 5),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: SpaceDropdown(
@ -94,6 +101,9 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
},
),
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
class DropdownMenuContent extends StatefulWidget {
final String? selectedSpaceId;
final ValueChanged<String?> onChanged;
final VoidCallback onClose;
const DropdownMenuContent({
required this.selectedSpaceId,
required this.onChanged,
required this.onClose,
});
@override
State<DropdownMenuContent> createState() => _DropdownMenuContentState();
}
class _DropdownMenuContentState extends State<DropdownMenuContent> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _onScroll() {
final bloc = context.read<SpaceTreeBloc>();
final state = bloc.state;
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 30) {
if (state is SpaceTreeState && !state.paginationIsLoading) {
bloc.add(PaginationEvent(state.paginationModel, state.communityList));
}
}
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Search bar
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: _searchController,
onChanged: (query) {
context.read<SpaceTreeBloc>().add(SearchQueryEvent(query));
},
style: const TextStyle(fontSize: 14, color: Colors.black),
decoration: InputDecoration(
hintText: 'Search for space...',
prefixIcon: const Icon(Icons.search, size: 20),
contentPadding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
),
),
// Community list
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount:
communities.length + (state.paginationIsLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= communities.length) {
return state.paginationIsLoading
? const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(strokeWidth: 2),
),
),
)
: const SizedBox.shrink();
}
final community = communities[index];
final isSelected = community.uuid == widget.selectedSpaceId;
return ListTile(
title: Text(
community.name,
style: TextStyle(
color: isSelected ? Colors.blue : Colors.black,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
),
),
onTap: () {
setState(() {
_searchController.clear();
_searchController.text.isEmpty
? context
.read<SpaceTreeBloc>()
.add(SearchQueryEvent(''))
: context.read<SpaceTreeBloc>().add(
SearchQueryEvent(_searchController.text));
});
// Future.delayed(const Duration(seconds: 1), () {
widget.onChanged(community.uuid);
widget.onClose();
// });
},
);
},
),
),
],
);
},
),
);
}
}

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceDropdown extends StatelessWidget {
final List<SpaceModel> spaces;
final String? selectedValue;
@ -21,7 +20,7 @@ class SpaceDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10.0),
padding: const EdgeInsets.only(left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -33,7 +32,6 @@ class SpaceDropdown extends StatelessWidget {
color: ColorsManager.blackColor,
),
),
const SizedBox(height: 8),
SizedBox(
child: Container(
decoration: BoxDecoration(
@ -90,7 +88,7 @@ class SpaceDropdown extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
flex: 6,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(

View File

@ -0,0 +1,27 @@
import 'package:flutter_bloc/flutter_bloc.dart';
part 'space_tree_dropdown_event.dart';
part 'space_tree_dropdown_state.dart';
class SpaceTreeDropdownBloc
extends Bloc<SpaceTreeDropdownEvent, SpaceTreeDropdownState> {
SpaceTreeDropdownBloc(String? initialId)
: super(SpaceTreeDropdownState(selectedSpaceId: initialId)) {
on<SpaceTreeDropdownSelectEvent>(_onSelect);
on<SpaceTreeDropdownResetEvent>(_onReset);
}
void _onSelect(
SpaceTreeDropdownSelectEvent event,
Emitter<SpaceTreeDropdownState> emit,
) {
emit(SpaceTreeDropdownState(selectedSpaceId: event.spaceId));
}
void _onReset(
SpaceTreeDropdownResetEvent event,
Emitter<SpaceTreeDropdownState> emit,
) {
emit(SpaceTreeDropdownState(selectedSpaceId: event.initialId));
}
}

View File

@ -0,0 +1,15 @@
part of 'space_tree_dropdown_bloc.dart';
abstract class SpaceTreeDropdownEvent {}
class SpaceTreeDropdownSelectEvent extends SpaceTreeDropdownEvent {
final String? spaceId;
SpaceTreeDropdownSelectEvent(this.spaceId);
}
class SpaceTreeDropdownResetEvent extends SpaceTreeDropdownEvent {
final String? initialId;
SpaceTreeDropdownResetEvent(this.initialId);
}

View File

@ -0,0 +1,7 @@
part of 'space_tree_dropdown_bloc.dart';
class SpaceTreeDropdownState {
final String? selectedSpaceId;
SpaceTreeDropdownState({this.selectedSpaceId});
}