diff --git a/assets/icons/active_user.svg b/assets/icons/active_user.svg new file mode 100644 index 00000000..5e0806e0 --- /dev/null +++ b/assets/icons/active_user.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg new file mode 100644 index 00000000..2b4be77b --- /dev/null +++ b/assets/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow_forward.svg b/assets/icons/arrow_forward.svg new file mode 100644 index 00000000..e5866360 --- /dev/null +++ b/assets/icons/arrow_forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/atoz_icon.png b/assets/icons/atoz_icon.png new file mode 100644 index 00000000..33a9c351 Binary files /dev/null and b/assets/icons/atoz_icon.png differ diff --git a/assets/icons/box_checked.png b/assets/icons/box_checked.png new file mode 100644 index 00000000..d93b9d76 Binary files /dev/null and b/assets/icons/box_checked.png differ diff --git a/assets/icons/compleate_process_icon.svg b/assets/icons/compleate_process_icon.svg new file mode 100644 index 00000000..a4159de2 --- /dev/null +++ b/assets/icons/compleate_process_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/current_process_icon.svg b/assets/icons/current_process_icon.svg new file mode 100644 index 00000000..967928e3 --- /dev/null +++ b/assets/icons/current_process_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/deactive_user.svg b/assets/icons/deactive_user.svg new file mode 100644 index 00000000..7011f5fb --- /dev/null +++ b/assets/icons/deactive_user.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/empty_box.png b/assets/icons/empty_box.png new file mode 100644 index 00000000..71e79875 Binary files /dev/null and b/assets/icons/empty_box.png differ diff --git a/assets/icons/filter_table_icon.svg b/assets/icons/filter_table_icon.svg new file mode 100644 index 00000000..d90e983e --- /dev/null +++ b/assets/icons/filter_table_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/invited_icon.svg b/assets/icons/invited_icon.svg new file mode 100644 index 00000000..5563de14 --- /dev/null +++ b/assets/icons/invited_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 00000000..9cd75905 --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/rectangle_check_box.png b/assets/icons/rectangle_check_box.png new file mode 100644 index 00000000..3404c79c Binary files /dev/null and b/assets/icons/rectangle_check_box.png differ diff --git a/assets/icons/search_icon_user.svg b/assets/icons/search_icon_user.svg new file mode 100644 index 00000000..61eca62d --- /dev/null +++ b/assets/icons/search_icon_user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/uncompleate_process_icon.svg b/assets/icons/uncompleate_process_icon.svg new file mode 100644 index 00000000..4ede6757 --- /dev/null +++ b/assets/icons/uncompleate_process_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/user_management.svg b/assets/icons/user_management.svg new file mode 100644 index 00000000..3255117a --- /dev/null +++ b/assets/icons/user_management.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/wrong_process_icon.svg b/assets/icons/wrong_process_icon.svg new file mode 100644 index 00000000..de5b475c --- /dev/null +++ b/assets/icons/wrong_process_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/ztoa_icon.png b/assets/icons/ztoa_icon.png new file mode 100644 index 00000000..003e5725 Binary files /dev/null and b/assets/icons/ztoa_icon.png differ diff --git a/lib/common/dialog_dropdown.dart b/lib/common/dialog_dropdown.dart new file mode 100644 index 00000000..7274b3c0 --- /dev/null +++ b/lib/common/dialog_dropdown.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogDropdown extends StatefulWidget { + final List items; + final ValueChanged onSelected; + final String? selectedValue; + + const DialogDropdown({ + Key? key, + required this.items, + required this.onSelected, + this.selectedValue, + }) : super(key: key); + + @override + _DialogDropdownState createState() => _DialogDropdownState(); +} + +class _DialogDropdownState extends State { + bool _isOpen = false; + late OverlayEntry _overlayEntry; + + @override + void initState() { + super.initState(); + } + + void _toggleDropdown() { + if (_isOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + } + + void _openDropdown() { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry); + _isOpen = true; + } + + void _closeDropdown() { + _overlayEntry.remove(); + _isOpen = false; + } + + OverlayEntry _createOverlayEntry() { + final renderBox = context.findRenderObject() as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (context) { + return GestureDetector( + onTap: () { + _closeDropdown(); + }, + behavior: HitTestBehavior.translucent, + child: Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy + size.height, + width: size.width, + child: Material( + elevation: 4.0, + child: Container( + color: ColorsManager.whiteColors, + constraints: const BoxConstraints( + maxHeight: 200.0, // Set max height for dropdown + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.items.length, + itemBuilder: (context, index) { + final item = widget.items[index]; + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 1.0, + ), + ), + ), + child: ListTile( + title: Text( + item, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager.textPrimaryColor, + ), + ), + onTap: () { + widget.onSelected(item); + _closeDropdown(); + }, + ), + ); + }, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _toggleDropdown, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.transparentColor), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.selectedValue ?? 'Select an item', + style: Theme.of(context).textTheme.bodyMedium, + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); + } +} diff --git a/lib/common/dialog_textfield_dropdown.dart b/lib/common/dialog_textfield_dropdown.dart new file mode 100644 index 00000000..807f3417 --- /dev/null +++ b/lib/common/dialog_textfield_dropdown.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogTextfieldDropdown extends StatefulWidget { + final List items; + final ValueChanged onSelected; + final String? initialValue; + + const DialogTextfieldDropdown({ + Key? key, + required this.items, + required this.onSelected, + this.initialValue, + }) : super(key: key); + + @override + _DialogTextfieldDropdownState createState() => + _DialogTextfieldDropdownState(); +} + +class _DialogTextfieldDropdownState extends State { + bool _isOpen = false; + late OverlayEntry _overlayEntry; + final TextEditingController _controller = TextEditingController(); + late List _filteredItems; // Filtered items list + + @override + void initState() { + super.initState(); + _controller.text = widget.initialValue ?? 'Select Tag'; + _filteredItems = List.from(widget.items); // Initialize filtered items + } + + void _toggleDropdown() { + if (_isOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + } + + void _openDropdown() { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry); + _isOpen = true; + } + + void _closeDropdown() { + _overlayEntry.remove(); + _isOpen = false; + } + + OverlayEntry _createOverlayEntry() { + final renderBox = context.findRenderObject() as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (context) { + return GestureDetector( + onTap: () { + _closeDropdown(); + }, + behavior: HitTestBehavior.translucent, + child: Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy + size.height, + width: size.width, + child: Material( + elevation: 4.0, + child: Container( + color: ColorsManager.whiteColors, + constraints: const BoxConstraints( + maxHeight: 200.0, + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: _filteredItems.length, + itemBuilder: (context, index) { + final item = _filteredItems[index]; + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 1.0, + ), + ), + ), + child: ListTile( + title: Text(item, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager.textPrimaryColor)), + onTap: () { + _controller.text = item; + widget.onSelected(item); + setState(() { + _filteredItems + .remove(item); // Remove selected item + }); + _closeDropdown(); + }, + ), + ); + }, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _toggleDropdown, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.transparentColor), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextFormField( + controller: _controller, + onChanged: (value) { + setState(() { + _filteredItems = widget.items + .where((item) => + item.toLowerCase().contains(value.toLowerCase())) + .toList(); // Filter items dynamically + }); + widget.onSelected(value); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: const InputDecoration( + hintText: 'Enter or Select tag', + border: InputBorder.none, + ), + ), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); + } +} diff --git a/lib/pages/auth/model/user_model.dart b/lib/pages/auth/model/user_model.dart index 84d4661f..5090b0e0 100644 --- a/lib/pages/auth/model/user_model.dart +++ b/lib/pages/auth/model/user_model.dart @@ -10,6 +10,10 @@ class UserModel { final String? phoneNumber; final bool? isEmailVerified; final bool? isAgreementAccepted; + final bool? hasAcceptedWebAgreement; + final DateTime? webAgreementAcceptedAt; + final UserRole? role; + UserModel({ required this.uuid, required this.email, @@ -19,6 +23,9 @@ class UserModel { required this.phoneNumber, required this.isEmailVerified, required this.isAgreementAccepted, + required this.hasAcceptedWebAgreement, + required this.webAgreementAcceptedAt, + required this.role, }); factory UserModel.fromJson(Map json) { @@ -31,6 +38,11 @@ class UserModel { phoneNumber: json['phoneNumber'], isEmailVerified: json['isEmailVerified'], isAgreementAccepted: json['isAgreementAccepted'], + hasAcceptedWebAgreement: json['hasAcceptedWebAgreement'], + webAgreementAcceptedAt: json['webAgreementAcceptedAt'] != null + ? DateTime.parse(json['webAgreementAcceptedAt']) + : null, + role: json['role'] != null ? UserRole.fromJson(json['role']) : null, ); } @@ -41,6 +53,9 @@ class UserModel { Map tempJson = Token.decodeToken(token.accessToken); return UserModel( + hasAcceptedWebAgreement: null, + role: null, + webAgreementAcceptedAt: null, uuid: tempJson['uuid'].toString(), email: tempJson['email'], firstName: null, @@ -65,3 +80,26 @@ class UserModel { }; } } + +class UserRole { + final String uuid; + final DateTime createdAt; + final DateTime updatedAt; + final String type; + + UserRole({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.type, + }); + + factory UserRole.fromJson(Map json) { + return UserRole( + uuid: json['uuid'], + createdAt: DateTime.parse(json['createdAt']), + updatedAt: DateTime.parse(json['updatedAt']), + type: json['type'], + ); + } +} diff --git a/lib/pages/common/custom_dialog.dart b/lib/pages/common/custom_dialog.dart index a40ef10f..9899bda4 100644 --- a/lib/pages/common/custom_dialog.dart +++ b/lib/pages/common/custom_dialog.dart @@ -12,7 +12,7 @@ Future showCustomDialog({ double? iconWidth, VoidCallback? onOkPressed, bool barrierDismissible = false, - required List actions, + List? actions, }) { return showDialog( context: context, diff --git a/lib/pages/device_managment/sos/view/sos_batch_control_view.dart b/lib/pages/device_managment/sos/view/sos_batch_control_view.dart index bc66d69f..9082c8bd 100644 --- a/lib/pages/device_managment/sos/view/sos_batch_control_view.dart +++ b/lib/pages/device_managment/sos/view/sos_batch_control_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/sos/bloc/sos_device_bloc.dart'; class SOSBatchControlView extends StatelessWidget { diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index 6863236b..08d35a24 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -18,10 +18,15 @@ class HomeBloc extends Bloc { // List sourcesList = []; // List destinationsList = []; UserModel? user; + String terms = ''; + String policy = ''; HomeBloc() : super((HomeInitial())) { // on(_createNode); on(_fetchUserInfo); + on(_fetchTerms); + on(_fetchPolicy); + on(_confirmUserAgreement); } // void _createNode(CreateNewNode event, Emitter emit) async { @@ -42,14 +47,48 @@ class HomeBloc extends Bloc { Future _fetchUserInfo(FetchUserInfo event, Emitter emit) async { try { - var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + var uuid = + await const FlutterSecureStorage().read(key: UserModel.userUuidKey); user = await HomeApi().fetchUserInfo(uuid); + add(FetchTermEvent()); emit(HomeInitial()); } catch (e) { return; } } + Future _fetchTerms(FetchTermEvent event, Emitter emit) async { + try { + emit(LoadingHome()); + terms = await HomeApi().fetchTerms(); + add(FetchPolicyEvent()); + } catch (e) { + return; + } + } + + Future _fetchPolicy(FetchPolicyEvent event, Emitter emit) async { + try { + emit(LoadingHome()); + policy = await HomeApi().fetchPolicy(); + } catch (e) { + return; + } + } + + Future _confirmUserAgreement( + ConfirmUserAgreementEvent event, Emitter emit) async { + try { + emit(LoadingHome()); + var uuid = + await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + policy = await HomeApi().confirmUserAgreements(uuid); + emit(PolicyAgreement()); + } catch (e) { + return; + } + } + // static Future fetchUserInfo() async { // try { // var uuid = diff --git a/lib/pages/home/bloc/home_event.dart b/lib/pages/home/bloc/home_event.dart index 50480602..b90ae5de 100644 --- a/lib/pages/home/bloc/home_event.dart +++ b/lib/pages/home/bloc/home_event.dart @@ -21,3 +21,9 @@ abstract class HomeEvent extends Equatable { class FetchUserInfo extends HomeEvent { const FetchUserInfo(); } + +class FetchTermEvent extends HomeEvent {} + +class FetchPolicyEvent extends HomeEvent {} + +class ConfirmUserAgreementEvent extends HomeEvent {} diff --git a/lib/pages/home/bloc/home_state.dart b/lib/pages/home/bloc/home_state.dart index 64c840ab..832820fd 100644 --- a/lib/pages/home/bloc/home_state.dart +++ b/lib/pages/home/bloc/home_state.dart @@ -8,8 +8,14 @@ abstract class HomeState extends Equatable { List get props => []; } +class LoadingHome extends HomeState {} + class HomeInitial extends HomeState {} +class TermsAgreement extends HomeState {} + +class PolicyAgreement extends HomeState {} + // class HomeCounterState extends HomeState { // final int counter; // const HomeCounterState(this.counter); diff --git a/lib/pages/home/view/agreement_and_privacy_dialog.dart b/lib/pages/home/view/agreement_and_privacy_dialog.dart new file mode 100644 index 00000000..e9371ae9 --- /dev/null +++ b/lib/pages/home/view/agreement_and_privacy_dialog.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/routes_const.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AgreementAndPrivacyDialog extends StatefulWidget { + final String terms; + final String policy; + + const AgreementAndPrivacyDialog({ + super.key, + required this.terms, + required this.policy, + }); + + @override + _AgreementAndPrivacyDialogState createState() => + _AgreementAndPrivacyDialogState(); +} + +class _AgreementAndPrivacyDialogState extends State { + final ScrollController _scrollController = ScrollController(); + bool _isAtEnd = false; + int _currentPage = 1; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + WidgetsBinding.instance + .addPostFrameCallback((_) => _checkScrollRequirement()); + } + + void _checkScrollRequirement() { + final scrollPosition = _scrollController.position; + if (scrollPosition.maxScrollExtent <= 0) { + setState(() { + _isAtEnd = true; + }); + } + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.atEdge) { + final isAtBottom = _scrollController.position.pixels == + _scrollController.position.maxScrollExtent; + if (isAtBottom && !_isAtEnd) { + setState(() { + _isAtEnd = true; + }); + } + } + } + + String get _dialogTitle => + _currentPage == 2 ? 'User Agreement' : 'Privacy Policy'; + + String get _dialogContent => _currentPage == 2 ? widget.terms : widget.policy; + + Widget _buildScrollableContent() { + return Container( + padding: const EdgeInsets.all(40), + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.75, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(25), + child: Html( + data: _dialogContent, + onLinkTap: (url, attributes, element) async { + if (url != null) { + final uri = Uri.parse(url); + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + }, + style: { + "body": Style( + fontSize: FontSize(14), + color: Colors.black87, + lineHeight: LineHeight(1.5), + ), + }, + ), + ), + ), + ); + } + + Widget _buildActionButton() { + final String buttonText = _currentPage == 2 ? "I Agree" : "Next"; + + return InkWell( + onTap: _isAtEnd + ? () { + if (_currentPage == 1) { + setState(() { + _currentPage = 2; + _isAtEnd = false; + _scrollController.jumpTo(0); + WidgetsBinding.instance + .addPostFrameCallback((_) => _checkScrollRequirement()); + }); + } else { + Navigator.of(context).pop(true); + } + } + : null, + child: Text( + buttonText, + style: TextStyle( + color: _isAtEnd ? ColorsManager.secondaryColor : Colors.grey, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + _dialogTitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorsManager.secondaryColor, + ), + ), + ), + const Divider(), + _buildScrollableContent(), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + InkWell( + onTap: () { + AuthBloc.logout(); + context.go(RoutesConst.auth); + }, + child: const Text("Cancel"), + ), + _buildActionButton(), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index 866f9766..3e4ad932 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/home/bloc/home_event.dart'; +import 'package:syncrow_web/pages/home/view/agreement_and_privacy_dialog.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/pages/home/view/home_card.dart'; @@ -9,16 +11,40 @@ import 'package:syncrow_web/web_layout/web_scaffold.dart'; class HomeWebPage extends StatelessWidget { const HomeWebPage({super.key}); + @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; + final homeBloc = BlocProvider.of(context); + return PopScope( canPop: false, onPopInvoked: (didPop) => false, child: BlocConsumer( - listener: (BuildContext context, state) {}, + listener: (BuildContext context, state) { + if (state is HomeInitial) { + if (homeBloc.user!.hasAcceptedWebAgreement == false) { + Future.delayed(const Duration(seconds: 1), () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AgreementAndPrivacyDialog( + terms: homeBloc.terms, + policy: homeBloc.policy, + ); + }, + ).then((v) { + if (v != null) { + homeBloc.add(ConfirmUserAgreementEvent()); + homeBloc.add(const FetchUserInfo()); + } + }); + }); + } + } + }, builder: (context, state) { - final homeBloc = BlocProvider.of(context); return WebScaffold( enableMenuSidebar: false, appBarTitle: Row( diff --git a/lib/pages/roles_and_permission/bloc/roles_permission_bloc.dart b/lib/pages/roles_and_permission/bloc/roles_permission_bloc.dart new file mode 100644 index 00000000..4f4988b3 --- /dev/null +++ b/lib/pages/roles_and_permission/bloc/roles_permission_bloc.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/bloc/roles_permission_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/bloc/roles_permission_state.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/role_model.dart'; + +class RolesPermissionBloc + extends Bloc { + RolesPermissionBloc() : super(RolesInitial()) { + on(_getRoles); + on(changeTapSelected); + } + List roleModel = []; + + FutureOr _getRoles( + GetRoles event, Emitter emit) async { + emit(UsersLoadingState()); + try { + roleModel = [ + RoleModel(roleId: '1', roleImage: '', roleName: 'Admin'), + RoleModel(roleId: '2', roleImage: '', roleName: 'Security'), + RoleModel(roleId: '2', roleImage: '', roleName: 'Reception'), + ]; + emit(UsersLoadedState()); + } catch (e) { + emit(ErrorState(e.toString())); + } + } + + bool tapSelect = true; + + changeTapSelected( + ChangeTapSelected event, Emitter emit) { + try { + emit(RolesLoadingState()); + tapSelect = event.selected; + emit(ChangeTapStatus(select: !tapSelect)); + } catch (e) { + emit(ErrorState(e.toString())); + } + } +} diff --git a/lib/pages/roles_and_permission/bloc/roles_permission_event.dart b/lib/pages/roles_and_permission/bloc/roles_permission_event.dart new file mode 100644 index 00000000..d5dce346 --- /dev/null +++ b/lib/pages/roles_and_permission/bloc/roles_permission_event.dart @@ -0,0 +1,41 @@ +import 'package:equatable/equatable.dart'; + +sealed class RolesPermissionEvent extends Equatable { + const RolesPermissionEvent(); +} + +class GetRoles extends RolesPermissionEvent { + const GetRoles(); + @override + List get props => []; +} + +class GetBatchStatus extends RolesPermissionEvent { + final List uuids; + const GetBatchStatus(this.uuids); + @override + List get props => [uuids]; +} + +class GetDeviceRecords extends RolesPermissionEvent { + final String uuid; + + const GetDeviceRecords(this.uuid); + @override + List get props => [uuid]; +} + +class GetDeviceAutomationRecords extends RolesPermissionEvent { + final String uuid; + const GetDeviceAutomationRecords(this.uuid); + @override + List get props => [uuid]; +} + +class ChangeTapSelected extends RolesPermissionEvent { + final bool selected; + const ChangeTapSelected(this.selected); + + @override + List get props => [selected]; +} diff --git a/lib/pages/roles_and_permission/bloc/roles_permission_state.dart b/lib/pages/roles_and_permission/bloc/roles_permission_state.dart new file mode 100644 index 00000000..7979f7db --- /dev/null +++ b/lib/pages/roles_and_permission/bloc/roles_permission_state.dart @@ -0,0 +1,79 @@ +import 'package:equatable/equatable.dart'; + +sealed class RolesPermissionState extends Equatable { + const RolesPermissionState(); +} + +final class RolesInitial extends RolesPermissionState { + @override + List get props => []; +} + +final class RolesLoadingState extends RolesPermissionState { + @override + List get props => []; +} + +final class UsersLoadingState extends RolesPermissionState { + @override + List get props => []; +} + +final class RolesLoadedState extends RolesPermissionState { + @override + List get props => []; +} + +final class UsersLoadedState extends RolesPermissionState { + @override + List get props => []; +} + +final class ErrorState extends RolesPermissionState { + final String message; + + const ErrorState(this.message); + + @override + List get props => [message]; +} + +/// report state +final class SosReportLoadingState extends RolesPermissionState { + @override + List get props => []; +} + +final class RolesErrorState extends RolesPermissionState { + final String message; + + const RolesErrorState(this.message); + + @override + List get props => [message]; +} + +/// automation reports + +final class SosAutomationReportLoadingState extends RolesPermissionState { + @override + List get props => []; +} + +final class SosAutomationReportErrorState extends RolesPermissionState { + final String message; + + const SosAutomationReportErrorState(this.message); + + @override + List get props => [message]; +} + +final class ChangeTapStatus extends RolesPermissionState { + final bool select; + + const ChangeTapStatus({required this.select}); + + @override + List get props => [select]; +} diff --git a/lib/pages/roles_and_permission/model/edit_user_model.dart b/lib/pages/roles_and_permission/model/edit_user_model.dart new file mode 100644 index 00000000..4075ac12 --- /dev/null +++ b/lib/pages/roles_and_permission/model/edit_user_model.dart @@ -0,0 +1,267 @@ +// import 'dart:convert'; + +// // Model for Space +// class UserSpaceModel { +// final String uuid; +// final DateTime createdAt; +// final DateTime updatedAt; + +// UserSpaceModel({ +// required this.uuid, +// required this.createdAt, +// required this.updatedAt, +// }); + +// factory UserSpaceModel.fromJson(Map json) { +// return UserSpaceModel( +// uuid: json['uuid'], +// createdAt: DateTime.parse(json['createdAt']), +// updatedAt: DateTime.parse(json['updatedAt']), +// ); +// } + +// Map toJson() { +// return { +// 'uuid': uuid, +// 'createdAt': createdAt.toIso8601String(), +// 'updatedAt': updatedAt.toIso8601String(), +// }; +// } +// } + +// // Model for User +// class EditUserModel { +// final String uuid; +// final DateTime createdAt; +// final dynamic email; +// final dynamic? jobTitle; +// final dynamic status; +// final String firstName; +// final String lastName; +// final String? phoneNumber; +// final bool isEnabled; +// final dynamic invitedBy; +// final dynamic roleType; +// final List spaces; +// final String createdDate; +// final String createdTime; + +// EditUserModel({ +// required this.uuid, +// required this.createdAt, +// required this.email, +// this.jobTitle, +// required this.status, +// required this.firstName, +// required this.lastName, +// this.phoneNumber, +// required this.isEnabled, +// required this.invitedBy, +// required this.roleType, +// required this.spaces, +// required this.createdDate, +// required this.createdTime, +// }); + +// factory EditUserModel.fromJson(Map json) { +// var spacesList = (json['spaces'] as List) +// .map((spaceJson) => UserSpaceModel.fromJson(spaceJson)) +// .toList(); + +// return EditUserModel( +// uuid: json['uuid'], +// createdAt: DateTime.parse(json['createdAt']), +// email: json['email'], +// jobTitle: json['jobTitle'], +// status: json['status'], +// firstName: json['firstName'], +// lastName: json['lastName'], +// phoneNumber: json['phoneNumber'], +// isEnabled: json['isEnabled'], +// invitedBy: json['invitedBy'], +// roleType: json['roleType'], +// spaces: spacesList, +// createdDate: json['createdDate'], +// createdTime: json['createdTime'], +// ); +// } + +// Map toJson() { +// return { +// 'uuid': uuid, +// 'createdAt': createdAt.toIso8601String(), +// 'email': email, +// 'jobTitle': jobTitle, +// 'status': status, +// 'firstName': firstName, +// 'lastName': lastName, +// 'phoneNumber': phoneNumber, +// 'isEnabled': isEnabled, +// 'invitedBy': invitedBy, +// 'roleType': roleType, +// 'spaces': spaces.map((space) => space.toJson()).toList(), +// 'createdDate': createdDate, +// 'createdTime': createdTime, +// }; +// } +// } + +class UserProjectResponse { + final int statusCode; + final String message; + final EditUserModel data; + final bool success; + + UserProjectResponse({ + required this.statusCode, + required this.message, + required this.data, + required this.success, + }); + + /// Create a [UserProjectResponse] from JSON data + factory UserProjectResponse.fromJson(Map json) { + return UserProjectResponse( + statusCode: json['statusCode'] as int, + message: json['message'] as String, + data: EditUserModel.fromJson(json['data'] as Map), + success: json['success'] as bool, + ); + } + + /// Convert the [UserProjectResponse] to JSON + Map toJson() { + return { + 'statusCode': statusCode, + 'message': message, + 'data': data.toJson(), + 'success': success, + }; + } +} + +class EditUserModel { + final String uuid; + final String firstName; + final String lastName; + final String email; + final String createdDate; // e.g. "1/3/2025" + final String createdTime; // e.g. "8:41:43 AM" + final String status; // e.g. "invited" + final String invitedBy; // e.g. "SUPER_ADMIN" + final String? phoneNumber; // can be empty + final String? jobTitle; // can be empty + final String roleType; // e.g. "ADMIN" + final List spaces; + + EditUserModel({ + required this.uuid, + required this.firstName, + required this.lastName, + required this.email, + required this.createdDate, + required this.createdTime, + required this.status, + required this.invitedBy, + required this.phoneNumber, + required this.jobTitle, + required this.roleType, + required this.spaces, + }); + + /// Create a [UserData] from JSON data + factory EditUserModel.fromJson(Map json) { + return EditUserModel( + uuid: json['uuid'] as String, + firstName: json['firstName'] as String, + lastName: json['lastName'] as String, + email: json['email'] as String, + createdDate: json['createdDate'] as String, + createdTime: json['createdTime'] as String, + status: json['status'] as String, + invitedBy: json['invitedBy'] as String, + phoneNumber: json['phoneNumber'] ?? '', + jobTitle: json['jobTitle'] ?? '', + roleType: json['roleType'] as String, + spaces: (json['spaces'] as List) + .map((e) => UserSpaceModel.fromJson(e as Map)) + .toList(), + ); + } + + /// Convert the [UserData] to JSON + Map toJson() { + return { + 'uuid': uuid, + 'firstName': firstName, + 'lastName': lastName, + 'email': email, + 'createdDate': createdDate, + 'createdTime': createdTime, + 'status': status, + 'invitedBy': invitedBy, + 'phoneNumber': phoneNumber, + 'jobTitle': jobTitle, + 'roleType': roleType, + 'spaces': spaces.map((e) => e.toJson()).toList(), + }; + } +} + +class UserSpaceModel { + final String uuid; + final String createdAt; // e.g. "2024-11-04T07:20:35.940Z" + final String updatedAt; // e.g. "2024-11-28T18:47:29.736Z" + final dynamic spaceTuyaUuid; + final dynamic spaceName; + final dynamic invitationCode; + final bool disabled; + final double x; + final double y; + final String icon; + + UserSpaceModel({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.spaceTuyaUuid, + required this.spaceName, + required this.invitationCode, + required this.disabled, + required this.x, + required this.y, + required this.icon, + }); + + /// Create a [UserSpaceModel] from JSON data + factory UserSpaceModel.fromJson(Map json) { + return UserSpaceModel( + uuid: json['uuid'] as String, + createdAt: json['createdAt'] as String, + updatedAt: json['updatedAt'] as String, + spaceTuyaUuid: json['spaceTuyaUuid'] as String?, + spaceName: json['spaceName'] as String, + invitationCode: json['invitationCode'] as String?, + disabled: json['disabled'] as bool, + x: (json['x'] as num).toDouble(), + y: (json['y'] as num).toDouble(), + icon: json['icon'] as String, + ); + } + + /// Convert the [UserSpaceModel] to JSON + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'spaceTuyaUuid': spaceTuyaUuid, + 'spaceName': spaceName, + 'invitationCode': invitationCode, + 'disabled': disabled, + 'x': x, + 'y': y, + 'icon': icon, + }; + } +} diff --git a/lib/pages/roles_and_permission/model/role_model.dart b/lib/pages/roles_and_permission/model/role_model.dart new file mode 100644 index 00000000..3d139904 --- /dev/null +++ b/lib/pages/roles_and_permission/model/role_model.dart @@ -0,0 +1,6 @@ +class RoleModel { + String? roleId; + String? roleName; + String? roleImage; + RoleModel({this.roleId, this.roleName, this.roleImage}); +} diff --git a/lib/pages/roles_and_permission/model/role_type_model.dart b/lib/pages/roles_and_permission/model/role_type_model.dart new file mode 100644 index 00000000..16b24ec5 --- /dev/null +++ b/lib/pages/roles_and_permission/model/role_type_model.dart @@ -0,0 +1,22 @@ +class RoleTypeModel { + final String uuid; + final String createdAt; + final String updatedAt; + final String type; + + RoleTypeModel({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.type, + }); + + factory RoleTypeModel.fromJson(Map json) { + return RoleTypeModel( + uuid: json['uuid'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + type: json['type'].toString().toLowerCase().replaceAll("_", " "), + ); + } +} diff --git a/lib/pages/roles_and_permission/model/roles_user_model.dart b/lib/pages/roles_and_permission/model/roles_user_model.dart new file mode 100644 index 00000000..44e3f493 --- /dev/null +++ b/lib/pages/roles_and_permission/model/roles_user_model.dart @@ -0,0 +1,50 @@ +class RolesUserModel { + final String uuid; + final DateTime createdAt; + final String email; + final dynamic firstName; + final dynamic lastName; + final dynamic roleType; + final dynamic status; + final bool isEnabled; + final String invitedBy; + final dynamic phoneNumber; + final dynamic jobTitle; + final dynamic createdDate; + final dynamic createdTime; + + RolesUserModel({ + required this.uuid, + required this.createdAt, + required this.email, + required this.firstName, + required this.lastName, + required this.roleType, + required this.status, + required this.isEnabled, + required this.invitedBy, + this.phoneNumber, + this.jobTitle, + required this.createdDate, + required this.createdTime, + }); + + factory RolesUserModel.fromJson(Map json) { + return RolesUserModel( + uuid: json['uuid'], + createdAt: DateTime.parse(json['createdAt']), + email: json['email'], + firstName: json['firstName'], + lastName: json['lastName'], + roleType: json['roleType'].toString().toLowerCase().replaceAll("_", " "), + status: json['status'], + isEnabled: json['isEnabled'], + invitedBy: + json['invitedBy'].toString().toLowerCase().replaceAll("_", " "), + phoneNumber: json['phoneNumber'], + jobTitle: json['jobTitle'] ?? "-", + createdDate: json['createdDate'], + createdTime: json['createdTime'], + ); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart new file mode 100644 index 00000000..26a1bcc7 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart @@ -0,0 +1,478 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/custom_dialog.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/edit_user_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/role_type_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; +import 'package:syncrow_web/services/user_permission.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class UsersBloc extends Bloc { + UsersBloc() : super(UsersInitial()) { + on(isCompleteBasicsFun); + on(_onLoadCommunityAndSpaces); + on(searchTreeNode); + on(isCompleteSpacesFun); + on(_getRolePermission); + on(_getPermissions); + on(searchRolePermission); + on(_sendInvitUser); + on(_validateBasicsStep); + on(isCompleteRoleFun); + on(checkEmail); + on(getUserById); + on(_onToggleNodeExpansion); + on(_onToggleNodeCheck); + on(_editInviteUser); + } + + void _validateBasicsStep(ValidateBasicsStep event, Emitter emit) { + if (formKey.currentState?.validate() ?? false) { + emit(const BasicsStepValidState()); + } else { + emit(const BasicsStepInvalidState()); + } + } + + String roleSelected = ''; + + final formKey = GlobalKey(); + final TextEditingController firstNameController = TextEditingController(); + final TextEditingController lastNameController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController phoneController = TextEditingController(); + final TextEditingController jobTitleController = TextEditingController(); + final TextEditingController roleSearchController = TextEditingController(); + + bool? isCompleteBasics; + bool? isCompleteRolePermissions; + bool? isCompleteSpaces; + + int numberBasics = 0; + int numberSpaces = 0; + int numberRole = 0; + + void isCompleteSpacesFun( + CheckSpacesStepStatus event, Emitter emit) { + emit(UsersLoadingState()); + List selectedIds = getSelectedIds(updatedCommunities); + isCompleteSpaces = selectedIds.isNotEmpty; + emit(ChangeStatusSteps()); + } + + void isCompleteRoleFun(CheckRoleStepStatus event, Emitter emit) { + emit(UsersLoadingState()); + isCompleteRolePermissions = roleSelected != ''; + emit(ChangeStatusSteps()); + } + + Future> _fetchSpacesForCommunity( + String communityUuid) async { + return await CommunitySpaceManagementApi().getSpaceHierarchy(communityUuid); + } + + List updatedCommunities = []; + List spacesNodes = []; + + _onLoadCommunityAndSpaces( + LoadCommunityAndSpacesEvent event, Emitter emit) async { + try { + emit(UsersLoadingState()); + List communities = + await CommunitySpaceManagementApi().fetchCommunities(); + updatedCommunities = await Future.wait( + communities.map((community) async { + List spaces = + await _fetchSpacesForCommunity(community.uuid); + spacesNodes = _buildTreeNodes(spaces); + return TreeNode( + uuid: community.uuid, + title: community.name, + children: spacesNodes, + isChecked: false, + isHighlighted: false, + isExpanded: true, + ); + }).toList(), + ); + emit(const SpacesLoadedState()); + return updatedCommunities; + } catch (e) { + emit(ErrorState('Error loading communities and spaces: $e')); + } + } + + List _buildTreeNodes(List spaces) { + return spaces.map((space) { + List childNodes = + space.children.isNotEmpty ? _buildTreeNodes(space.children) : []; + return TreeNode( + uuid: space.uuid!, + title: space.name, + isChecked: false, + isHighlighted: false, + isExpanded: childNodes.isNotEmpty, + children: childNodes, + ); + }).toList(); + } + + void searchTreeNode(SearchAnode event, Emitter emit) { + emit(UsersLoadingState()); + if (event.searchTerm!.isEmpty) { + _clearHighlights(updatedCommunities); + } else { + _searchAndHighlightNodes(updatedCommunities, event.searchTerm!); + } + emit(ChangeStatusSteps()); + } + + void _clearHighlights(List nodes) { + for (var node in nodes) { + node.isHighlighted = false; + if (node.children.isNotEmpty) { + _clearHighlights(node.children); + } + } + } + + bool _searchAndHighlightNodes(List nodes, String searchTerm) { + bool anyMatch = false; + for (var node in nodes) { + bool isMatch = + node.title.toLowerCase().contains(searchTerm.toLowerCase()); + bool childMatch = _searchAndHighlightNodes(node.children, searchTerm); + node.isHighlighted = isMatch || childMatch; + + anyMatch = anyMatch || node.isHighlighted; + } + return anyMatch; + } + + List selectedIds = []; + + List getSelectedIds(List nodes) { + List selectedIds = []; + for (var node in nodes) { + if (node.isChecked) { + selectedIds.add(node.uuid); + } + if (node.children.isNotEmpty) { + selectedIds.addAll(getSelectedIds(node.children)); + } + } + return selectedIds; + } + + List roles = []; + List permissions = []; + + _getRolePermission(RoleEvent event, Emitter emit) async { + try { + emit(UsersLoadingState()); + roles = await UserPermissionApi().fetchRoles(); + // add(PermissionEvent(roleUuid: roles.first.uuid)); + emit(RolePermissionInitial()); + } catch (e) { + emit(ErrorState('Error loading communities and spaces: $e')); + } + } + + _getPermissions(PermissionEvent event, Emitter emit) async { + try { + emit(UsersLoadingState()); + permissions = await UserPermissionApi().fetchPermission( + event.roleUuid == "" ? roles.first.uuid : event.roleUuid); + roleSelected = event.roleUuid!; + emit(RolePermissionInitial()); + } catch (e) { + emit(ErrorState('Error loading communities and spaces: $e')); + } + } + + bool _searchRolePermission(List nodes, String searchTerm) { + bool anyMatch = false; + for (var node in nodes) { + bool isMatch = + node.title.toLowerCase().contains(searchTerm.toLowerCase()); + bool childMatch = _searchRolePermission(node.subOptions, searchTerm); + node.isHighlighted = isMatch || childMatch; + anyMatch = anyMatch || node.isHighlighted; + } + return anyMatch; + } + + _sendInvitUser(SendInviteUsers event, Emitter emit) async { + try { + emit(UsersLoadingState()); + List selectedIds = getSelectedIds(updatedCommunities); + bool res = await UserPermissionApi().sendInviteUser( + email: emailController.text, + firstName: firstNameController.text, + jobTitle: jobTitleController.text, + lastName: lastNameController.text, + phoneNumber: phoneController.text, + roleUuid: roleSelected, + spaceUuids: selectedIds, + ); + if (res == true) { + showCustomDialog( + barrierDismissible: false, + context: event.context, + message: "The invite was sent successfully.", + iconPath: Assets.deviceNoteIcon, + title: "Invite Success", + dialogHeight: MediaQuery.of(event.context).size.height * 0.3, + actions: [ + TextButton( + onPressed: () { + Navigator.of(event.context).pop(true); + Navigator.of(event.context).pop(true); + }, + child: const Text('OK'), + ), + ], + ); + } else { + emit(const ErrorState('Failed to send invite.')); + } + emit(SaveState()); + } catch (e) { + emit(ErrorState('Failed to send invite: ${e.toString()}')); + } + } + + _editInviteUser(EditInviteUsers event, Emitter emit) async { + try { + emit(UsersLoadingState()); + List selectedIds = getSelectedIds(updatedCommunities); + bool res = await UserPermissionApi().editInviteUser( + userId: event.userId, + firstName: firstNameController.text, + jobTitle: jobTitleController.text, + lastName: lastNameController.text, + phoneNumber: phoneController.text, + roleUuid: roleSelected, + spaceUuids: selectedIds, + ); + if (res == true) { + showCustomDialog( + barrierDismissible: false, + context: event.context, + message: "The invite was sent successfully.", + iconPath: Assets.deviceNoteIcon, + title: "Invite Success", + dialogHeight: MediaQuery.of(event.context).size.height * 0.3, + actions: [ + TextButton( + onPressed: () { + Navigator.of(event.context).pop(true); + Navigator.of(event.context).pop(true); + }, + child: const Text('OK'), + ), + ], + ).then( + (value) {}, + ); + } else { + emit(const ErrorState('Failed to send invite.')); + } + emit(SaveState()); + } catch (e) { + emit(ErrorState('Failed to send invite: ${e.toString()}')); + } + } + + void searchRolePermission(SearchPermission event, Emitter emit) { + emit(UsersLoadingState()); + if (event.searchTerm!.isEmpty) { + _clearHighlightsRolePermission(permissions); + } else { + _searchRolePermission(permissions, event.searchTerm!); + } + emit(ChangeStatusSteps()); + } + + String checkEmailValid = ''; + + Future checkEmail( + CheckEmailEvent event, Emitter emit) async { + emit(UsersLoadingState()); + String? res = await UserPermissionApi().checkEmail( + emailController.text, + ); + checkEmailValid = res!; + emit(ChangeStatusSteps()); + } + + bool isCompleteBasicsFun(CheckStepStatus event, Emitter emit) { + emit(UsersLoadingState()); + if (event.isEditUser == false) { + add(const CheckEmailEvent()); + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + bool isEmailValid = emailRegex.hasMatch(emailController.text); + bool isEmailServerValid = checkEmailValid == 'Valid email'; + isCompleteBasics = firstNameController.text.isNotEmpty && + lastNameController.text.isNotEmpty && + emailController.text.isNotEmpty && + isEmailValid && + isEmailServerValid; + } else { + isCompleteBasics = firstNameController.text.isNotEmpty && + lastNameController.text.isNotEmpty; + } + emit(ChangeStatusSteps()); + emit(ValidateBasics()); + return isCompleteBasics!; + } + + void _clearHighlightsRolePermission(List nodes) { + for (var node in nodes) { + node.isHighlighted = false; + if (node.subOptions.isNotEmpty) { + _clearHighlightsRolePermission(node.subOptions); + } + } + } + + EditUserModel? res = EditUserModel( + spaces: [], + jobTitle: '', + phoneNumber: '', + uuid: '', + email: '', + firstName: '', + lastName: '', + roleType: '', + status: '', + invitedBy: '', + createdDate: '', + createdTime: ''); + + Future getUserById( + GetUserByIdEvent event, + Emitter emit, + ) async { + emit(UsersLoadingState()); + + try { + if (event.uuid?.isNotEmpty ?? false) { + final res = await UserPermissionApi().fetchUserById(event.uuid); + + if (res != null) { + // Populate the text controllers + firstNameController.text = res.firstName; + lastNameController.text = res.lastName; + emailController.text = res.email; + phoneController.text = res.phoneNumber ?? ''; + jobTitleController.text = res.jobTitle ?? ''; + res.roleType; + if (updatedCommunities.isNotEmpty) { + // Create a list of UUIDs to mark + final uuidsToMark = res.spaces.map((space) => space.uuid).toList(); + // Print all IDs and mark nodes in updatedCommunities + debugPrint('Printing and marking nodes in updatedCommunities:'); + _printAndMarkNodes(updatedCommunities, uuidsToMark); + } + final roleId = roles + .firstWhere((element) => + element.type == + res.roleType.toString().toLowerCase().replaceAll("_", " ")) + .uuid; + debugPrint('Role ID: $roleId'); + roleSelected = roleId; + add(PermissionEvent(roleUuid: roleSelected)); + emit(ChangeStatusSteps()); + } else {} + } else {} + } catch (_) {} + } + + void _printAndMarkNodes(List nodes, List uuidsToMark, + [int level = 0]) { + for (final node in nodes) { + if (uuidsToMark.contains(node.uuid)) { + node.isChecked = true; + debugPrint( + '${' ' * level}MATCH FOUND: Node ID: ${node.uuid}, Title: ${node.title} is marked as checked.'); + } else { + debugPrint( + '${' ' * level}Node ID: ${node.uuid}, Title: ${node.title}'); + } + if (node.children.isNotEmpty) { + _printAndMarkNodes(node.children, uuidsToMark, level + 1); + } + } + } + + void _onToggleNodeExpansion( + ToggleNodeExpansion event, + Emitter emit, + ) { + emit(UsersLoadingState()); + event.node.isExpanded = !event.node.isExpanded; + emit(ChangeStatusSteps()); + } + + void _onToggleNodeCheck( + ToggleNodeCheck event, + Emitter emit, + ) { + emit(UsersLoadingState()); + //Toggle node's checked state + event.node.isChecked = !event.node.isChecked; + debugPrint( + 'Node toggled. ID: ${event.node.uuid}, isChecked: ${event.node.isChecked}', + ); + // Update children and parent + _updateChildrenCheckStatus(event.node, event.node.isChecked); + _updateParentCheckStatus(event.node); + + // Finally, emit a new state + emit(ChangeStatusSteps()); + } + + void _updateChildrenCheckStatus(TreeNode node, bool isChecked) { + for (var child in node.children) { + child.isChecked = isChecked; + _updateChildrenCheckStatus(child, isChecked); + } + } + + void _updateParentCheckStatus(TreeNode node) { + TreeNode? parent = _findParent(updatedCommunities, node); + if (parent != null) { + parent.isChecked = _areAllChildrenChecked(parent); + _updateParentCheckStatus(parent); + } + } + + bool _areAllChildrenChecked(TreeNode node) { + return node.children.isNotEmpty && + node.children.every((child) => + child.isChecked && + (child.children.isEmpty || _areAllChildrenChecked(child))); + } + + TreeNode? _findParent(List nodes, TreeNode target) { + for (var node in nodes) { + if (node.children.contains(target)) { + return node; + } + final parent = _findParent(node.children, target); + if (parent != null) { + return parent; + } + } + return null; + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart new file mode 100644 index 00000000..2e82168c --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart @@ -0,0 +1,176 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart'; + +sealed class UsersEvent extends Equatable { + const UsersEvent(); +} + +class SendInviteUsers extends UsersEvent { + final BuildContext context; + const SendInviteUsers({required this.context}); + @override + List get props => [context]; +} + +class EditInviteUsers extends UsersEvent { + final BuildContext context; + final String userId; + const EditInviteUsers({required this.context, required this.userId}); + @override + List get props => [context, userId]; +} + +class CheckSpacesStepStatus extends UsersEvent { + const CheckSpacesStepStatus(); + @override + List get props => []; +} + +class CheckRoleStepStatus extends UsersEvent { + const CheckRoleStepStatus(); + @override + List get props => []; +} + +class LoadCommunityAndSpacesEvent extends UsersEvent { + const LoadCommunityAndSpacesEvent(); + @override + List get props => []; +} + +class RoleEvent extends UsersEvent { + const RoleEvent(); + @override + List get props => []; +} + +class PermissionEvent extends UsersEvent { + final String? roleUuid; + const PermissionEvent({this.roleUuid = ""}); + @override + List get props => [roleUuid]; +} + +class GetBatchStatus extends UsersEvent { + final List uuids; + const GetBatchStatus(this.uuids); + @override + List get props => [uuids]; +} + +//isEditUser:widget.userId!=''? false:true +class CheckStepStatus extends UsersEvent { + final int? steps; + final bool? isEditUser; + const CheckStepStatus({this.steps, required this.isEditUser}); + @override + List get props => [steps]; +} + +class SearchAnode extends UsersEvent { + final List? nodes; + final String? searchTerm; + const SearchAnode({this.nodes, this.searchTerm}); + @override + List get props => [nodes, searchTerm]; +} + +class SearchPermission extends UsersEvent { + final List? nodes; + final String? searchTerm; + const SearchPermission({this.nodes, this.searchTerm}); + @override + List get props => [nodes, searchTerm]; +} + +class SelectedId extends UsersEvent { + final List? nodes; + const SelectedId({ + this.nodes, + }); + @override + List get props => [nodes]; +} + +class ValidateBasicsStep extends UsersEvent { + const ValidateBasicsStep(); + @override + List get props => []; +} + +class CheckEmailEvent extends UsersEvent { + const CheckEmailEvent(); + @override + List get props => []; +} + +class GetUserByIdEvent extends UsersEvent { + final String? uuid; + const GetUserByIdEvent({this.uuid}); + @override + List get props => [uuid]; +} + +class ToggleNodeExpansion extends UsersEvent { + final TreeNode node; + + const ToggleNodeExpansion({required this.node}); + + @override + List get props => [node]; +} + +class UpdateNodeCheckStatus extends UsersEvent { + final TreeNode node; + + const UpdateNodeCheckStatus({required this.node}); + @override + List get props => [node]; +} + +class ToggleNodeHighlightEvent extends UsersEvent { + final TreeNode node; + + const ToggleNodeHighlightEvent(this.node); + @override + List get props => [node]; +} + +class ExpandAllNodesEvent extends UsersEvent { + @override + List get props => []; +} + +class CollapseAllNodesEvent extends UsersEvent { + @override + List get props => []; +} + +class ClearSelectionsEvent extends UsersEvent { + @override + List get props => []; +} + +class ToggleNodeCheckEvent extends UsersEvent { + final TreeNode node; + + const ToggleNodeCheckEvent(this.node); + @override + List get props => []; +} + +class ToggleNodeCheck extends UsersEvent { + final TreeNode node; + + const ToggleNodeCheck(this.node); + @override + List get props => []; +} + +class EditUserEvent extends UsersEvent { + const EditUserEvent(); + @override + List get props => []; +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart new file mode 100644 index 00000000..4361c37f --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart @@ -0,0 +1,91 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart'; + +sealed class UsersState extends Equatable { + const UsersState(); +} + +final class UsersInitial extends UsersState { + @override + List get props => []; +} + +final class RolePermissionInitial extends UsersState { + @override + List get props => []; +} + +final class ChangeStatusSteps extends UsersState { + @override + List get props => []; +} + +final class UsersLoadingState extends UsersState { + @override + List get props => []; +} + +final class SaveState extends UsersState { + @override + List get props => []; +} + +final class SpacesLoadedState extends UsersState { + const SpacesLoadedState(); + @override + List get props => []; +} + +final class ErrorState extends UsersState { + final String message; + + const ErrorState(this.message); + + @override + List get props => [message]; +} + +final class RolesErrorState extends UsersState { + final String message; + + const RolesErrorState(this.message); + + @override + List get props => [message]; +} + +/// automation reports + +final class ChangeTapStatus extends UsersState { + final bool select; + + const ChangeTapStatus({required this.select}); + + @override + List get props => [select]; +} + +final class BasicsStepValidState extends UsersState { + const BasicsStepValidState(); + @override + List get props => []; +} + +class BasicsStepInvalidState extends UsersState { + const BasicsStepInvalidState(); + @override + List get props => []; +} + +final class ValidateBasics extends UsersState { + @override + List get props => []; +} + +class UsersLoadedState extends UsersState { + final List updatedCommunities; + + const UsersLoadedState({required this.updatedCommunities}); + @override + List get props => []; +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart new file mode 100644 index 00000000..4141ccdd --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart @@ -0,0 +1,42 @@ +class PermissionOption { + String id; + String title; + bool isChecked; + bool isHighlighted; + List subOptions; + + PermissionOption({ + required this.id, + required this.title, + this.isChecked = false, + this.isHighlighted = false, + this.subOptions = const [], + }); + + factory PermissionOption.fromJson(Map json) { + return PermissionOption( + id: json['id'] ?? '', + title: json['title'] != null + ? json['title'].toString().toLowerCase().replaceAll("_", " ") + : '', + isChecked: json['isChecked'] ?? false, + isHighlighted: json['isHighlighted'] ?? false, + subOptions: (json['subOptions'] as List?) + ?.map((sub) => PermissionOption.fromJson(sub)) + .toList() ?? + [], + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'isChecked': isChecked, + 'isHighlighted': isHighlighted, + 'subOptions': subOptions.map((sub) => sub.toJson()).toList(), + }; + } +} + +enum CheckState { none, some, all } diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart new file mode 100644 index 00000000..a5e622dc --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart @@ -0,0 +1,17 @@ +class TreeNode { + String uuid; + String title; + bool isChecked; + bool isHighlighted; + bool isExpanded; + List children; + + TreeNode({ + required this.uuid, + required this.title, + this.isChecked = false, + this.isHighlighted = false, + this.isExpanded = false, + this.children = const [], + }); +} \ No newline at end of file diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart new file mode 100644 index 00000000..ec35b3fd --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart @@ -0,0 +1,360 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/roles_and_permission.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/spaces_access_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class AddNewUserDialog extends StatefulWidget { + const AddNewUserDialog({super.key}); + + @override + _AddNewUserDialogState createState() => _AddNewUserDialogState(); +} + +class _AddNewUserDialogState extends State { + int currentStep = 1; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => UsersBloc() + ..add(const LoadCommunityAndSpacesEvent()) + ..add(const RoleEvent()), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + final _blocRole = BlocProvider.of(context); + + return Dialog( + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), + width: 900, + child: Column( + children: [ + // Title + const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox( + child: Text( + "Add New User", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorsManager.secondaryColor), + ), + ), + ), + const Divider(), + Expanded( + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStep1Indicator(1, "Basics", _blocRole), + _buildStep2Indicator(2, "Spaces", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), + ], + ), + ), + ), + Container( + width: 1, + color: ColorsManager.grayBorder, + ), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Expanded( + child: _getFormContent(), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ], + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ), + InkWell( + onTap: () { + _blocRole.add(const CheckEmailEvent()); + + setState(() { + if (currentStep < 3) { + currentStep++; + if (currentStep == 2) { + _blocRole.add( + const CheckStepStatus(isEditUser: false)); + } else if (currentStep == 3) { + _blocRole + .add(const CheckSpacesStepStatus()); + } + } else { + _blocRole + .add(SendInviteUsers(context: context)); + } + }); + }, + child: Text( + currentStep < 3 ? "Next" : "Save", + style: TextStyle( + color: (_blocRole.isCompleteSpaces == false || + _blocRole.isCompleteBasics == + false || + _blocRole + .isCompleteRolePermissions == + false) && + currentStep == 3 + ? ColorsManager.grayColor + : ColorsManager.secondaryColor), + ), + ), + ], + ), + ), + ], + ), + )); + })); + } + + Widget _getFormContent() { + switch (currentStep) { + case 1: + return const BasicsView( + userId: '', + ); + case 2: + return const SpacesAccessView(); + case 3: + return const RolesAndPermission(); + default: + return Container(); + } + } + + int step3 = 0; + + Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + bloc.add(const CheckSpacesStepStatus()); + currentStep = step; + Future.delayed(const Duration(milliseconds: 500), () { + bloc.add(const ValidateBasicsStep()); + }); + }); + + if (step3 == 3) { + bloc.add(const CheckRoleStepStatus()); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteBasics == false + ? Assets.wrongProcessIcon + : bloc.isCompleteBasics == true + ? Assets.completeProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } + + Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + currentStep = step; + bloc.add(const CheckStepStatus(isEditUser: false)); + if (step3 == 3) { + bloc.add(const CheckRoleStepStatus()); + } + + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteSpaces == false + ? Assets.wrongProcessIcon + : bloc.isCompleteSpaces == true + ? Assets.completeProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } + + Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + currentStep = step; + step3 = step; + bloc.add(const CheckSpacesStepStatus()); + bloc.add(CheckStepStatus(isEditUser: false)); + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteRolePermissions == false + ? Assets.wrongProcessIcon + : bloc.isCompleteRolePermissions == true + ? Assets.completeProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart new file mode 100644 index 00000000..53d9a333 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart @@ -0,0 +1,339 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl_phone_field/countries.dart'; +import 'package:intl_phone_field/country_picker_dialog.dart'; +import 'package:intl_phone_field/intl_phone_field.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class BasicsView extends StatelessWidget { + final String? userId; + const BasicsView({super.key, this.userId = ''}); + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + final _blocRole = BlocProvider.of(context); + if (state is BasicsStepInvalidState) { + _blocRole.formKey.currentState?.validate(); + } + return Form( + key: _blocRole.formKey, + child: ListView( + shrinkWrap: true, + children: [ + Text( + 'Set up the basics', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 20, + color: Colors.black), + ), + const SizedBox( + height: 50, + ), + Text( + 'To get started, fill out some basic information about who you’re adding as a user.', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const SizedBox( + height: 35, + ), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.18, + height: MediaQuery.of(context).size.width * 0.08, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Row( + children: [ + const Text( + " * ", + style: TextStyle( + color: ColorsManager.red, + fontWeight: FontWeight.w900, + fontSize: 15, + ), + ), + Text( + 'First Name', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 13, + ), + ), + ], + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + style: + const TextStyle(color: ColorsManager.blackColor), + // onChanged: (value) { + // Future.delayed(const Duration(milliseconds: 200), + // () { + // _blocRole.add(const ValidateBasicsStep()); + // }); + // }, + controller: _blocRole.firstNameController, + decoration: inputTextFormDeco( + hintText: "Enter first name", + ).copyWith( + hintStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Enter first name'; + } + return null; + }, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + SizedBox( + width: MediaQuery.of(context).size.width * 0.18, + height: MediaQuery.of(context).size.width * 0.08, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Row( + children: [ + const Text( + " * ", + style: TextStyle( + color: ColorsManager.red, + fontWeight: FontWeight.w900, + fontSize: 15, + ), + ), + Text('Last Name', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 13, + )), + ], + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + // onChanged: (value) { + // Future.delayed(const Duration(milliseconds: 200), + // () { + // _blocRole.add(ValidateBasicsStep()); + // }); + // }, + controller: _blocRole.lastNameController, + style: const TextStyle(color: Colors.black), + decoration: + inputTextFormDeco(hintText: "Enter last name") + .copyWith( + hintStyle: context + .textTheme.bodyMedium + ?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray)), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Enter last name'; + } + return null; + }, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Row( + children: [ + const Text( + " * ", + style: TextStyle( + color: ColorsManager.red, + fontWeight: FontWeight.w900, + fontSize: 15, + ), + ), + Text( + 'Email Address', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 13, + ), + ), + ], + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + enabled: userId != '' ? false : true, + // onChanged: (value) { + // Future.delayed(const Duration(milliseconds: 200), () { + // _blocRole.add(CheckStepStatus( + // isEditUser: userId != '' ? false : true)); + // _blocRole.add(ValidateBasicsStep()); + // }); + // }, + controller: _blocRole.emailController, + style: const TextStyle(color: ColorsManager.blackColor), + decoration: inputTextFormDeco(hintText: "name@example.com") + .copyWith( + hintStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Enter Email Address'; + } + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + if (!emailRegex.hasMatch(value)) { + return 'Enter a valid Email Address'; + } + if (_blocRole.checkEmailValid != "Valid email") { + return _blocRole.checkEmailValid; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Row( + children: [ + Text( + 'Mobile Number', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 13, + ), + ), + ], + )), + IntlPhoneField( + pickerDialogStyle: PickerDialogStyle(), + dropdownIconPosition: IconPosition.leading, + disableLengthCheck: true, + dropdownTextStyle: + const TextStyle(color: ColorsManager.blackColor), + textInputAction: TextInputAction.done, + decoration: inputTextFormDeco( + hintText: "05x xxx xxxx", + ).copyWith( + hintStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray), + ), + initialCountryCode: 'AE', + countries: const [ + Country( + name: "United Arab Emirates", + nameTranslations: { + "en": "United Arab Emirates", + "ar": "الإمارات العربية المتحدة", + }, + flag: "🇦🇪", + code: "AE", + dialCode: "971", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Saudi Arabia", + nameTranslations: { + "en": "Saudi Arabia", + "ar": "السعودية", + }, + flag: "🇸🇦", + code: "SA", + dialCode: "966", + minLength: 9, + maxLength: 9, + ), + ], + style: const TextStyle(color: Colors.black), + controller: _blocRole.phoneController, + ) + ], + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: Row( + children: [ + Text( + 'Job Title', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 13, + ), + ), + ], + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _blocRole.jobTitleController, + style: + const TextStyle(color: ColorsManager.blackColor), + decoration: inputTextFormDeco( + hintText: "Job Title (Optional)") + .copyWith( + hintStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray), + ), + keyboardType: TextInputType.phone, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ); + }); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/build_tree_view.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/build_tree_view.dart new file mode 100644 index 00000000..b7fc1085 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/build_tree_view.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/tree_node_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class TreeView extends StatelessWidget { + final String? userId; + + const TreeView({ + super.key, + this.userId, + }); + + @override + Widget build(BuildContext context) { + final _blocRole = BlocProvider.of(context); + debugPrint('TreeView constructed with userId = $userId'); + return BlocProvider( + create: (_) => UsersBloc(), + // ..add(const LoadCommunityAndSpacesEvent()), + child: BlocConsumer( + listener: (context, state) { + // if (state is SpacesLoadedState) { + // _blocRole.add(GetUserByIdEvent(uuid: userId)); + // } + }, + builder: (context, state) { + if (state is UsersLoadingState) { + return const Center(child: CircularProgressIndicator()); + } + return SingleChildScrollView( + child: _buildTree(_blocRole.updatedCommunities, _blocRole), + ); + }, + ), + ); + } + + Widget _buildTree( + List nodes, + UsersBloc bloc, { + int level = 0, + }) { + return Column( + children: nodes.map((node) { + return Container( + color: node.isHighlighted ? Colors.blue.shade50 : Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + /// Checkbox (GestureDetector) + GestureDetector( + onTap: () { + bloc.add(ToggleNodeCheck(node)); + }, + child: Image.asset( + _getCheckBoxImage(node), + width: 20, + height: 20, + ), + ), + const SizedBox(width: 15), + Expanded( + child: Padding( + padding: EdgeInsets.only(left: level * 10.0), + child: Row( + children: [ + GestureDetector( + onTap: () { + bloc.add(ToggleNodeExpansion(node: node)); + }, + child: node.children.isNotEmpty + ? SvgPicture.asset( + node.isExpanded + ? Assets.arrowDown + : Assets.arrowForward, + fit: BoxFit.none, + ) + : const SizedBox(width: 16), + ), + const SizedBox(width: 20), + Text( + node.title, + style: TextStyle( + fontSize: 16, + color: node.isHighlighted + ? ColorsManager.blackColor + : ColorsManager.textGray, + ), + ), + ], + ), + ), + ), + ], + ), + ), + if (node.isExpanded) + _buildTree( + node.children, + bloc, + level: level + 1, + ), + ], + ), + ); + }).toList(), + ); + } + + String _getCheckBoxImage(TreeNode node) { + if (node.children.isEmpty) { + return node.isChecked ? Assets.CheckBoxChecked : Assets.emptyBox; + } + if (_areAllChildrenChecked(node)) { + return Assets.CheckBoxChecked; + } else if (_areSomeChildrenChecked(node)) { + return Assets.rectangleCheckBox; + } else { + return Assets.emptyBox; + } + } + + bool _areAllChildrenChecked(TreeNode node) { + return node.children.isNotEmpty && + node.children.every((child) => + child.isChecked && + (child.children.isEmpty || _areAllChildrenChecked(child))); + } + + bool _areSomeChildrenChecked(TreeNode node) { + return node.children.isNotEmpty && + node.children.any((child) => + child.isChecked || + (child.children.isNotEmpty && _areSomeChildrenChecked(child))); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/delete_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/delete_user_dialog.dart new file mode 100644 index 00000000..10e8c273 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/delete_user_dialog.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DeleteUserDialog extends StatefulWidget { + final Function()? onTapDelete; + DeleteUserDialog({super.key, this.onTapDelete}); + + @override + _DeleteUserDialogState createState() => _DeleteUserDialogState(); +} + +class _DeleteUserDialogState extends State { + bool isLoading = false; + bool _isDisposed = false; + + @override + void dispose() { + _isDisposed = true; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: Container( + height: 160, + width: 200, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox( + child: Text( + "Delete User", + style: TextStyle( + color: ColorsManager.red, + fontSize: 18, + fontWeight: FontWeight.bold), + ), + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 25, + right: 25, + ), + child: Divider(), + ), + const Expanded( + child: Padding( + padding: EdgeInsets.only(left: 25, right: 25, top: 10, bottom: 10), + child: Text( + "Are you sure you want to delete this user?", + textAlign: TextAlign.center, + ), + )), + Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.of(context).pop(false); // Return false if canceled + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + width: 0.5, + ), + top: BorderSide( + color: ColorsManager.grayBorder, + width: 1, + ), + ), + ), + child: const Center(child: Text('Cancel'))), + )), + Expanded( + child: InkWell( + onTap: isLoading + ? null + : () async { + setState(() { + isLoading = true; + }); + + try { + if (widget.onTapDelete != null) { + await widget.onTapDelete!(); + } + } finally { + if (!_isDisposed) { + setState(() { + isLoading = false; + }); + } + } + Navigator.of(context).pop(true); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: ColorsManager.grayBorder, + width: 0.5, + ), + top: BorderSide( + color: ColorsManager.grayBorder, + width: 1, + ), + ), + ), + child: Center( + child: isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: ColorsManager.red, + strokeWidth: 2.0, + ), + ) + : const Text( + 'Delete', + style: TextStyle( + color: ColorsManager.red, + ), + ))), + )), + ], + ) + ], + ), + )); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart new file mode 100644 index 00000000..8b6600e0 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -0,0 +1,364 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/roles_and_permission.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/spaces_access_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class EditUserDialog extends StatefulWidget { + final String? userId; + const EditUserDialog({super.key, this.userId}); + + @override + _EditUserDialogState createState() => _EditUserDialogState(); +} + +class _EditUserDialogState extends State { + int currentStep = 1; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => UsersBloc() + ..add(const LoadCommunityAndSpacesEvent()) + ..add(const RoleEvent()) + ..add(GetUserByIdEvent(uuid: widget.userId)), + child: BlocConsumer(listener: (context, state) { + if (state is SpacesLoadedState) { + BlocProvider.of(context) + .add(GetUserByIdEvent(uuid: widget.userId)); + } + }, builder: (context, state) { + final _blocRole = BlocProvider.of(context); + + return Dialog( + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), + width: 900, + child: Column( + children: [ + // Title + const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox( + child: Text( + "Edit User", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorsManager.secondaryColor), + ), + ), + ), + const Divider(), + Expanded( + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStep1Indicator(1, "Basics", _blocRole), + _buildStep2Indicator(2, "Spaces", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), + ], + ), + ), + ), + Container( + width: 1, + color: ColorsManager.grayBorder, + ), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Expanded( + child: _getFormContent(widget.userId), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ], + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ), + InkWell( + onTap: () { + // _blocRole.add(const CheckEmailEvent()); + + setState(() { + if (currentStep < 3) { + currentStep++; + if (currentStep == 2) { + _blocRole + .add(CheckStepStatus(isEditUser: true)); + } else if (currentStep == 3) { + _blocRole.add(const CheckSpacesStepStatus()); + } + } else { + _blocRole.add(EditInviteUsers( + context: context, + userId: widget.userId!)); + } + }); + }, + child: Text( + currentStep < 3 ? "Next" : "Save", + style: TextStyle( + color: (_blocRole.isCompleteSpaces == false || + _blocRole.isCompleteBasics == false || + _blocRole.isCompleteRolePermissions == + false) && + currentStep == 3 + ? ColorsManager.grayColor + : ColorsManager.secondaryColor), + ), + ), + ], + ), + ), + ], + ), + )); + })); + } + + Widget _getFormContent(userid) { + switch (currentStep) { + case 1: + return BasicsView( + userId: userid, + ); + case 2: + return SpacesAccessView( + userId: userid, + ); + case 3: + return const RolesAndPermission(); + default: + return Container(); + } + } + + int step3 = 0; + + Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + bloc.add(const CheckSpacesStepStatus()); + currentStep = step; + Future.delayed(const Duration(milliseconds: 500), () { + bloc.add(ValidateBasicsStep()); + }); + }); + + if (step3 == 3) { + bloc.add(const CheckRoleStepStatus()); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteBasics == false + ? Assets.wrongProcessIcon + : bloc.isCompleteBasics == true + ? Assets.completeProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } + + Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + currentStep = step; + bloc.add(CheckStepStatus(isEditUser: true)); + if (step3 == 3) { + bloc.add(const CheckRoleStepStatus()); + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteSpaces == false + ? Assets.wrongProcessIcon + : bloc.isCompleteSpaces == true + ? Assets.completeProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } + + Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + currentStep = step; + step3 = step; + bloc.add(const CheckSpacesStepStatus()); + bloc.add(CheckStepStatus(isEditUser: true)); + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteRolePermissions == false + ? Assets.wrongProcessIcon + : bloc.isCompleteRolePermissions == true + ? Assets.completeProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/permission_management.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/permission_management.dart new file mode 100644 index 00000000..aee84ed4 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/permission_management.dart @@ -0,0 +1,295 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class PermissionManagement extends StatefulWidget { + final UsersBloc? bloc; + const PermissionManagement({Key? key, this.bloc}) : super(key: key); + + @override + _PermissionManagementState createState() => _PermissionManagementState(); +} + +class _PermissionManagementState extends State { + void toggleOptionById(String id) { + setState(() { + for (var mainOption in widget.bloc!.permissions) { + if (mainOption.id == id) { + final isChecked = + checkifOneOfthemChecked(mainOption) == CheckState.all; + mainOption.isChecked = !isChecked; + + for (var subOption in mainOption.subOptions) { + subOption.isChecked = !isChecked; + for (var child in subOption.subOptions) { + child.isChecked = !isChecked; + } + } + return; + } + + for (var subOption in mainOption.subOptions) { + if (subOption.id == id) { + subOption.isChecked = !subOption.isChecked; + for (var child in subOption.subOptions) { + child.isChecked = subOption.isChecked; + } + mainOption.isChecked = + mainOption.subOptions.every((sub) => sub.isChecked); + return; + } + + for (var child in subOption.subOptions) { + if (child.id == id) { + child.isChecked = !child.isChecked; + subOption.isChecked = + subOption.subOptions.every((child) => child.isChecked); + mainOption.isChecked = + mainOption.subOptions.every((sub) => sub.isChecked); + return; + } + } + } + } + }); + } + + CheckState checkifOneOfthemChecked(PermissionOption mainOption) { + bool allSelected = true; + bool someSelected = false; + + for (var subOption in mainOption.subOptions) { + if (subOption.isChecked) { + someSelected = true; + } else { + allSelected = false; + } + + for (var child in subOption.subOptions) { + if (child.isChecked) { + someSelected = true; + } else { + allSelected = false; + } + } + } + + if (allSelected) { + return CheckState.all; + } else if (someSelected) { + return CheckState.some; + } else { + return CheckState.none; + } + } + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: widget.bloc!.permissions.length, + itemBuilder: (context, index) { + final option = widget.bloc!.permissions[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + InkWell( + // onTap: () => toggleOptionById(option.id), + child: Builder( + builder: (context) { + final checkState = checkifOneOfthemChecked(option); + + if (checkState == CheckState.all) { + return Image.asset( + Assets.CheckBoxChecked, + width: 20, + height: 20, + ); + } else if (checkState == CheckState.some) { + return Image.asset( + Assets.rectangleCheckBox, + width: 20, + height: 20, + ); + } else { + return Image.asset( + Assets.emptyBox, + width: 20, + height: 20, + ); + } + }, + ), + ), + const SizedBox(width: 8), + Text( + ' ${option.title.isNotEmpty ? option.title[0].toUpperCase() : ''}${option.title.substring(1)}', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 12, + color: ColorsManager.blackColor), + ), + ], + ), + const SizedBox( + height: 10, + ), + ...option.subOptions.map((subOption) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: option.isHighlighted + ? Colors.blue.shade50 + : Colors.white, + child: Row( + children: [ + InkWell( + // onTap: () => toggleOptionById(subOption.id), + child: Builder( + builder: (context) { + final checkState = + checkifOneOfthemChecked(PermissionOption( + id: subOption.id, + title: subOption.title, + subOptions: [subOption], + )); + + if (checkState == CheckState.all) { + return Image.asset( + Assets.CheckBoxChecked, + width: 20, + height: 20, + ); + } else if (checkState == CheckState.some) { + return Image.asset( + Assets.rectangleCheckBox, + width: 20, + height: 20, + ); + } else { + return Image.asset( + Assets.emptyBox, + width: 20, + height: 20, + ); + } + }, + ), + ), + const SizedBox(width: 8), + Text( + ' ${subOption.title.isNotEmpty ? subOption.title[0].toUpperCase() : ''}${subOption.title.substring(1)}', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 12, + color: ColorsManager.lightGreyColor), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(left: 50.0), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 2.0, + crossAxisSpacing: 0.2, + childAspectRatio: 5, + ), + itemCount: subOption.subOptions.length, + itemBuilder: (context, index) { + final child = subOption.subOptions[index]; + return Container( + color: option.isHighlighted + ? Colors.blue.shade50 + : Colors.white, + child: Row( + children: [ + Builder( + builder: (context) { + final checkState = + checkifOneOfthemChecked(PermissionOption( + id: child.id, + title: child.title, + subOptions: [child], + )); + + if (checkState == CheckState.all) { + return Image.asset( + Assets.CheckBoxChecked, + width: 20, + height: 20, + ); + } else if (checkState == CheckState.some) { + return Image.asset( + Assets.rectangleCheckBox, + width: 20, + height: 20, + ); + } else { + return Image.asset( + Assets.emptyBox, + width: 20, + height: 20, + ); + } + }, + ), + const SizedBox(width: 8), + Text( + ' ${child.title.isNotEmpty ? child.title[0].toUpperCase() : ''}${child.title.substring(1)}', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.lightGreyColor), + ), + ], + ), + ); + }, + ), + ) + ], + ); + }).toList(), + ], + ); + }, + ); + } +} + + + // Container( + // height: 50, + // width: 120, + // child: CheckboxListTile( + // activeColor: ColorsManager.dialogBlueTitle, + // selectedTileColor: child.isHighlighted + // ? Colors.blue.shade50 + // : Colors.white, + // dense: true, + // controlAffinity: + // ListTileControlAffinity.leading, + // title: Text( + // child.title, + // style: context.textTheme.bodyMedium?.copyWith( + // fontWeight: FontWeight.w400, + // fontSize: 12, + // color: ColorsManager.lightGreyColor), + // ), + // value: child.isChecked, + // onChanged: (value) => + // toggleOptionById(child.id), + // enabled: false, + // ), + // ), \ No newline at end of file diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/popup_menu_filter.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/popup_menu_filter.dart new file mode 100644 index 00000000..120a1a3a --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/popup_menu_filter.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +Future showPopUpFilterMenu({ + required BuildContext context, + required Function(String value) onSortAtoZ, + required Function(String value) onSortZtoA, + Function()? cancelButton, + required Map checkboxStates, + required RelativeRect position, + Function()? onOkPressed, + List? list, + String? isSelected, +}) async { + await showMenu( + context: context, + position: position, + color: ColorsManager.whiteColors, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + items: [ + PopupMenuItem( + enabled: false, + child: StatefulBuilder( + builder: (context, setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + child: ListTile( + onTap: () { + setState(() { + if (isSelected == 'Asc') { + isSelected = null; + onSortAtoZ.call(''); + } else { + onSortAtoZ.call('Asc'); + isSelected = 'Asc'; + } + }); + }, + leading: Image.asset( + Assets.AtoZIcon, + width: 25, + ), + title: Text( + "Sort A to Z", + style: TextStyle( + color: isSelected == "Asc" + ? ColorsManager.blackColor + : ColorsManager.grayColor), + ), + ), + ), + ListTile( + onTap: () { + setState(() { + if (isSelected == 'Desc') { + isSelected = null; + onSortZtoA.call(''); + } else { + onSortZtoA.call('Desc'); + isSelected = 'Desc'; + } + }); + }, + leading: Image.asset( + Assets.ZtoAIcon, + width: 25, + ), + title: Text( + "Sort Z to A", + style: TextStyle( + color: isSelected == "Desc" + ? ColorsManager.blackColor + : ColorsManager.grayColor), + ), + ), + const Divider(), + const Text( + "Filter by Status", + style: TextStyle(fontWeight: FontWeight.bold), + ), + Container( + decoration: containerDecoration.copyWith( + boxShadow: [], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10))), + padding: const EdgeInsets.all(10), + height: 200, + width: 400, + child: Container( + padding: const EdgeInsets.all(10), + color: Colors.white, + child: ListView.builder( + itemCount: list?.length ?? 0, + itemBuilder: (context, index) { + final item = list![index]; + return Row( + children: [ + Checkbox( + value: checkboxStates[item], + onChanged: (bool? newValue) { + checkboxStates[item] = newValue ?? false; + (context as Element).markNeedsBuild(); + }, + ), + Text( + item, + style: TextStyle(color: ColorsManager.grayColor), + ), + ], + ); + }, + ), + ), + ), + const SizedBox( + height: 10, + ), + const Divider(), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(); // Close the menu + }, + child: const Text("Cancel"), + ), + GestureDetector( + onTap: onOkPressed, + child: const Text( + "OK", + style: TextStyle( + color: ColorsManager.spaceColor, + ), + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + ], + ); + }, + ), + ), + ], + ); +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/role_dropdown.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/role_dropdown.dart new file mode 100644 index 00000000..3a5ac65c --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/role_dropdown.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RoleDropdown extends StatefulWidget { + final UsersBloc? bloc; + const RoleDropdown({super.key, this.bloc}); + + @override + _RoleDropdownState createState() => _RoleDropdownState(); +} + +class _RoleDropdownState extends State { + late String selectedRole; + + @override + void initState() { + super.initState(); + selectedRole = widget.bloc!.roleSelected; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Text( + " * ", + style: TextStyle( + color: ColorsManager.red, + fontWeight: FontWeight.w900, + fontSize: 15, + ), + ), + Text( + "Role", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.black, + ), + ), + ], + ), + const SizedBox(height: 8), + SizedBox( + child: DropdownButtonFormField( + dropdownColor: ColorsManager.whiteColors, + // alignment: Alignment., + focusColor: Colors.white, + autofocus: true, + value: selectedRole.isNotEmpty ? selectedRole : null, + items: widget.bloc!.roles.map((role) { + return DropdownMenuItem( + value: role.uuid, + child: Text( + ' ${role.type.isNotEmpty ? role.type[0].toUpperCase() : ''}${role.type.substring(1)}', + ), + ); + }).toList(), + onChanged: (value) { + setState(() { + selectedRole = value!; + }); + widget.bloc!.roleSelected = selectedRole; + widget.bloc! + .add(PermissionEvent(roleUuid: widget.bloc!.roleSelected)); + }, + icon: const SizedBox.shrink(), + borderRadius: const BorderRadius.all(Radius.circular(10)), + hint: const Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + "Please Select", + style: TextStyle( + color: ColorsManager.textGray, + ), + ), + ), + decoration: inputTextFormDeco().copyWith( + contentPadding: EdgeInsets.zero, + suffixIcon: Container( + padding: EdgeInsets.zero, + width: 70, + height: 45, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(10), + topRight: Radius.circular(10), + ), + border: Border.all( + color: ColorsManager.textGray, + width: 1.0, + ), + ), + child: const Center( + child: Icon( + Icons.keyboard_arrow_down, + color: ColorsManager.textGray, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/roles_and_permission.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/roles_and_permission.dart new file mode 100644 index 00000000..f4b91747 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/roles_and_permission.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/permission_management.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/role_dropdown.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RolesAndPermission extends StatelessWidget { + const RolesAndPermission({super.key}); + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + final _blocRole = BlocProvider.of(context); + return Container( + color: Colors.white, + child: Form( + key: _blocRole.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + 'Role & Permissions', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 20, + color: Colors.black), + ), + const SizedBox( + height: 15, + ), + SizedBox( + width: 350, + height: 100, + child: RoleDropdown( + bloc: _blocRole, + )), + const SizedBox(height: 10), + Expanded( + child: SizedBox( + child: Column( + children: [ + Expanded( + flex: 2, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + topLeft: Radius.circular(20)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(20)), + border: Border.all( + color: ColorsManager.grayBorder)), + child: TextFormField( + style: + const TextStyle(color: Colors.black), + controller: + _blocRole.roleSearchController, + onChanged: (value) { + _blocRole.add(SearchPermission( + nodes: _blocRole.permissions, + searchTerm: value)); + }, + decoration: textBoxDecoration(radios: 20)! + .copyWith( + fillColor: Colors.white, + suffixIcon: Padding( + padding: + const EdgeInsets.only(right: 16), + child: SvgPicture.asset( + Assets.textFieldSearch, + width: 24, + height: 24, + ), + ), + hintStyle: context.textTheme.bodyMedium + ?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray), + ), + ), + ), + ), + ], + ), + ), + ), + ), + Expanded( + flex: 7, + child: Container( + color: ColorsManager.circleRolesBackground, + padding: const EdgeInsets.all(8.0), + child: Container( + color: ColorsManager.whiteColors, + child: PermissionManagement( + bloc: _blocRole, + )))) + ], + ), + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/spaces_access_view.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/spaces_access_view.dart new file mode 100644 index 00000000..f4ccfafc --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/spaces_access_view.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/build_tree_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpacesAccessView extends StatelessWidget { + final String? userId; + const SpacesAccessView({super.key, this.userId = ''}); + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + final _blocRole = BlocProvider.of(context); + return Container( + color: Colors.white, + child: Form( + key: _blocRole.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + 'Spaces access', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 20, + color: Colors.black), + ), + const SizedBox( + height: 35, + ), + const SizedBox( + child: Text( + 'Select the spaces you would like to grant access to for the user you are adding'), + ), + const SizedBox( + height: 25, + ), + Expanded( + child: SizedBox( + child: Column( + children: [ + Expanded( + flex: 2, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + topLeft: Radius.circular(20)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(20)), + border: Border.all( + color: ColorsManager.grayBorder)), + child: TextFormField( + style: + const TextStyle(color: Colors.black), + // controller: _blocRole.firstNameController, + onChanged: (value) { + _blocRole.add(SearchAnode( + nodes: _blocRole.updatedCommunities, + searchTerm: value)); + }, + decoration: textBoxDecoration(radios: 20)! + .copyWith( + fillColor: Colors.white, + suffixIcon: Padding( + padding: + const EdgeInsets.only(right: 16), + child: SvgPicture.asset( + Assets.textFieldSearch, + width: 24, + height: 24, + ), + ), + hintStyle: context.textTheme.bodyMedium + ?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray), + ), + ), + ), + ), + ], + ), + ), + ), + ), + Expanded( + flex: 7, + child: Container( + color: ColorsManager.circleRolesBackground, + padding: const EdgeInsets.all(8.0), + child: Container( + color: ColorsManager.whiteColors, + child: TreeView(userId: userId)))) + ], + ), + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_bloc.dart b/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_bloc.dart new file mode 100644 index 00000000..c50667be --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_bloc.dart @@ -0,0 +1,372 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_state.dart'; +import 'package:syncrow_web/services/user_permission.dart'; + +class UserTableBloc extends Bloc { + UserTableBloc() : super(TableInitial()) { + on(_getUsers); + on(_changeUserStatus); + on(_toggleSortUsersByNameAsc); + on(_toggleSortUsersByNameDesc); + on(_toggleSortUsersByDateOldestToNewest); + on(_toggleSortUsersByDateNewestToOldest); + on(_searchUsers); + on(_handlePageChange); + on(_filterUsersByRole); + on(_filterUsersByJobTitle); + on(_filterUsersByCreated); + on(_filterUserStatus); + on(_deleteUser); + on(_filterClear); + } + int itemsPerPage = 20; + int currentPage = 1; + List users = []; + List initialUsers = []; + String currentSortOrder = ''; + String currentSortOrderDate = ''; + List roleTypes = []; + List jobTitle = []; + List createdBy = []; + List status = ['active', 'invited', 'disabled']; + + Future _getUsers(GetUsers event, Emitter emit) async { + emit(UsersLoadingState()); + try { + roleTypes.clear(); + jobTitle.clear(); + createdBy.clear(); + // deActivate.clear(); + users = await UserPermissionApi().fetchUsers(); + + users.sort((a, b) { + final dateA = _parseDateTime(a.createdDate); + final dateB = _parseDateTime(b.createdDate); + return dateB.compareTo(dateA); + }); + for (var user in users) { + roleTypes.add(user.roleType.toString()); + } + for (var user in users) { + jobTitle.add(user.jobTitle.toString()); + } + for (var user in users) { + createdBy.add(user.invitedBy.toString()); + } + // for (var user in users) { + // deActivate.add(user.status.toString()); + // } + initialUsers = List.from(users); + roleTypes = roleTypes.toSet().toList(); + jobTitle = jobTitle.toSet().toList(); + createdBy = createdBy.toSet().toList(); + // deActivate = deActivate.toSet().toList(); + _handlePageChange(ChangePage(1), emit); + emit(UsersLoadedState(users: users)); + } catch (e) { + emit(ErrorState(e.toString())); + } + } + + Future _deleteUser( + DeleteUserEvent event, Emitter emit) async { + emit(UsersLoadingState()); + try { + bool res = await UserPermissionApi().deleteUserById(event.userId); + if (res == true) { + Navigator.of(event.context).pop(true); + } else { + emit(const ErrorState('Something error')); + } + emit(UsersLoadedState(users: users)); + } catch (e) { + emit(ErrorState(e.toString())); + } + } + + Future _changeUserStatus( + ChangeUserStatus event, Emitter emit) async { + try { + emit(UsersLoadingState()); + bool res = await UserPermissionApi().changeUserStatusById( + event.userId, event.newStatus == "disabled" ? false : true); + if (res == true) { + add(const GetUsers()); + // users = users.map((user) { + // if (user.uuid == event.userId) { + // return RolesUserModel( + // uuid: user.uuid, + // createdAt: user.createdAt, + // email: user.email, + // firstName: user.firstName, + // lastName: user.lastName, + // roleType: user.roleType, + // status: event.newStatus, + // isEnabled: event.newStatus == "disabled" ? false : true, + // invitedBy: user.invitedBy, + // phoneNumber: user.phoneNumber, + // jobTitle: user.jobTitle, + // createdDate: user.createdDate, + // createdTime: user.createdTime, + // ); + // } + // return user; + // }).toList(); + } + emit(UsersLoadedState(users: users)); + } catch (e) { + emit(ErrorState(e.toString())); + } + } + + void _toggleSortUsersByNameAsc( + SortUsersByNameAsc event, Emitter emit) { + if (currentSortOrder == "Asc") { + emit(UsersLoadingState()); + currentSortOrder = ""; + users = List.from(users); + emit(UsersLoadedState(users: users)); + } else { + emit(UsersLoadingState()); + currentSortOrder = "Asc"; + users.sort((a, b) => a.firstName + .toString() + .toLowerCase() + .compareTo(b.firstName.toString().toLowerCase())); + emit(UsersLoadedState(users: users)); + } + } + + void _toggleSortUsersByNameDesc( + SortUsersByNameDesc event, Emitter emit) { + if (currentSortOrder == "Desc") { + emit(UsersLoadingState()); + currentSortOrder = ""; + users = List.from(initialUsers); // Reset to saved initial state + emit(UsersLoadedState(users: users)); + } else { + // Sort descending + emit(UsersLoadingState()); + currentSortOrder = "Desc"; + users.sort((a, b) => b.firstName!.compareTo(a.firstName!)); + emit(UsersLoadedState(users: users)); + } + } + + void _toggleSortUsersByDateNewestToOldest( + DateNewestToOldestEvent event, Emitter emit) { + if (currentSortOrderDate == "NewestToOldest") { + emit(UsersLoadingState()); + currentSortOrder = ""; + currentSortOrderDate = ""; + users = List.from(initialUsers); + emit(UsersLoadedState(users: users)); + } else { + emit(UsersLoadingState()); + currentSortOrder = "NewestToOldest"; + users.sort((a, b) { + final dateA = _parseDateTime(a.createdDate); + final dateB = _parseDateTime(b.createdDate); + return dateB.compareTo(dateA); + }); + emit(UsersLoadedState(users: users)); + } + } + + void _toggleSortUsersByDateOldestToNewest( + DateOldestToNewestEvent event, Emitter emit) { + if (currentSortOrderDate == "OldestToNewest") { + emit(UsersLoadingState()); + currentSortOrder = ""; + currentSortOrderDate = ""; + users = List.from(initialUsers); + emit(UsersLoadedState(users: users)); + } else { + emit(UsersLoadingState()); + users.sort((a, b) { + final dateA = _parseDateTime(a.createdDate); + final dateB = _parseDateTime(b.createdDate); + return dateA.compareTo(dateB); + }); + currentSortOrder = "OldestToNewest"; + emit(UsersLoadedState(users: users)); + } + } + + DateTime _parseDateTime(String date) { + try { + final dateParts = date.split('/'); + final day = int.parse(dateParts[0]); + final month = int.parse(dateParts[1]); + final year = int.parse(dateParts[2]); + return DateTime(year, month, day); + } catch (e) { + throw FormatException('Invalid date or time format: $date '); + } + } + + Future _searchUsers( + SearchUsers event, Emitter emit) async { + try { + final query = event.query.toLowerCase(); + final filteredUsers = initialUsers.where((user) { + final fullName = "${user.firstName} ${user.lastName}".toLowerCase(); + final email = user.email.toLowerCase(); + return fullName.contains(query) || email.contains(query); + }).toList(); + emit(UsersLoadedState(users: filteredUsers)); + } catch (e) { + emit(ErrorState(e.toString())); + } + } + + void _paginateUsers( + int pageNumber, int itemsPerPage, Emitter emit) { + final startIndex = (pageNumber - 1) * itemsPerPage; + final endIndex = startIndex + itemsPerPage; + if (startIndex >= users.length) { + emit(const UsersLoadedState(users: [])); + return; + } + final paginatedUsers = users.sublist( + startIndex, + endIndex > users.length ? users.length : endIndex, + ); + emit(UsersLoadedState(users: paginatedUsers)); + } + + void _handlePageChange(ChangePage event, Emitter emit) { + const itemsPerPage = 10; + final startIndex = (event.pageNumber - 1) * itemsPerPage; + final endIndex = startIndex + itemsPerPage; + if (startIndex >= users.length) { + emit(const UsersLoadedState(users: [])); + return; + } + final paginatedUsers = users.sublist( + startIndex, + endIndex > users.length ? users.length : endIndex, + ); + emit(UsersLoadedState(users: paginatedUsers)); + } + + Set selectedRoles = {}; + Set selectedJobTitles = {}; + Set selectedCreatedBy = {}; + Set selectedStatuses = {}; + + void _filterUsersByRole( + FilterUsersByRoleEvent event, Emitter emit) { + selectedRoles = event.selectedRoles!.toSet(); + + final filteredUsers = initialUsers.where((user) { + if (selectedRoles.isEmpty) return true; + return selectedRoles.contains(user.roleType); + }).toList(); + + if (event.sortOrder == "Asc") { + currentSortOrder = "Asc"; + filteredUsers.sort((a, b) => a.firstName + .toString() + .toLowerCase() + .compareTo(b.firstName.toString().toLowerCase())); + } else if (event.sortOrder == "Desc") { + currentSortOrder = "Desc"; + filteredUsers.sort((a, b) => b.firstName!.compareTo(a.firstName!)); + } else { + currentSortOrder = ""; + } + + emit(UsersLoadedState(users: filteredUsers)); + } + + void _filterUsersByJobTitle( + FilterUsersByJobEvent event, Emitter emit) { + selectedJobTitles = event.selectedJob!.toSet(); + emit(UsersLoadingState()); + final filteredUsers = initialUsers.where((user) { + if (selectedJobTitles.isEmpty) return true; + return selectedJobTitles.contains(user.jobTitle); + }).toList(); + if (event.sortOrder == "Asc") { + currentSortOrder = "Asc"; + filteredUsers.sort((a, b) => a.firstName + .toString() + .toLowerCase() + .compareTo(b.firstName.toString().toLowerCase())); + } else if (event.sortOrder == "Desc") { + currentSortOrder = "Desc"; + filteredUsers.sort((a, b) => b.firstName!.compareTo(a.firstName!)); + } else { + currentSortOrder = ""; + } + emit(UsersLoadedState(users: filteredUsers)); + } + + void _filterUsersByCreated( + FilterUsersByCreatedEvent event, Emitter emit) { + selectedCreatedBy = event.selectedCreatedBy!.toSet(); + + final filteredUsers = initialUsers.where((user) { + if (selectedCreatedBy.isEmpty) return true; + return selectedCreatedBy.contains(user.invitedBy); + }).toList(); + + if (event.sortOrder == "Asc") { + currentSortOrder = "Asc"; + filteredUsers.sort((a, b) => a.firstName + .toString() + .toLowerCase() + .compareTo(b.firstName.toString().toLowerCase())); + } else if (event.sortOrder == "Desc") { + currentSortOrder = "Desc"; + filteredUsers.sort((a, b) => b.firstName!.compareTo(a.firstName!)); + } else { + currentSortOrder = ""; + } + emit(UsersLoadedState(users: filteredUsers)); + } + + void _filterUserStatus( + FilterUsersByDeActevateEvent event, Emitter emit) { + selectedStatuses = event.selectedActivate!.toSet(); + + final filteredUsers = initialUsers.where((user) { + if (selectedStatuses.isEmpty) return true; + return selectedStatuses.contains(user.status); + }).toList(); + if (event.sortOrder == "Asc") { + currentSortOrder = "Asc"; + filteredUsers.sort((a, b) => a.firstName + .toString() + .toLowerCase() + .compareTo(b.firstName.toString().toLowerCase())); + } else if (event.sortOrder == "Desc") { + currentSortOrder = "Desc"; + filteredUsers.sort((a, b) => b.firstName!.compareTo(a.firstName!)); + } else { + currentSortOrder = ""; + } + emit(UsersLoadedState(users: filteredUsers)); + } + + void _resetAllFilters(Emitter emit) { + selectedRoles.clear(); + selectedJobTitles.clear(); + selectedCreatedBy.clear(); + selectedStatuses.clear(); + emit(UsersLoadedState(users: initialUsers)); + } + + void _filterClear(FilterClearEvent event, Emitter emit) { + selectedRoles.clear(); + selectedJobTitles.clear(); + selectedCreatedBy.clear(); + selectedStatuses.clear(); + emit(UsersLoadedState(users: initialUsers)); + } +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_event.dart b/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_event.dart new file mode 100644 index 00000000..1d9567cf --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_event.dart @@ -0,0 +1,138 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; + +sealed class UserTableEvent extends Equatable { + const UserTableEvent(); +} + +class GetRoles extends UserTableEvent { + const GetRoles(); + @override + List get props => []; +} + +class GetUsers extends UserTableEvent { + const GetUsers(); + @override + List get props => []; +} + +class ChangeUserStatus extends UserTableEvent { + final String userId; + final String newStatus; + + const ChangeUserStatus({required this.userId, required this.newStatus}); + + @override + List get props => [userId, newStatus]; +} + +class SortUsersByNameAsc extends UserTableEvent { + const SortUsersByNameAsc(); + + @override + List get props => []; +} + +class SortUsersByNameDesc extends UserTableEvent { + const SortUsersByNameDesc(); + + @override + List get props => []; +} + +class StoreUsersEvent extends UserTableEvent { + const StoreUsersEvent(); + + @override + List get props => []; +} + +class DateNewestToOldestEvent extends UserTableEvent { + const DateNewestToOldestEvent(); + + @override + List get props => []; +} + +class DateOldestToNewestEvent extends UserTableEvent { + const DateOldestToNewestEvent(); + + @override + List get props => []; +} + +class SearchUsers extends UserTableEvent { + final String query; + SearchUsers(this.query); + @override + List get props => []; +} + +class ChangePage extends UserTableEvent { + final int pageNumber; + + ChangePage(this.pageNumber); + + @override + List get props => [pageNumber]; +} + +class DeleteUserEvent extends UserTableEvent { + final String userId; + final BuildContext context; + + const DeleteUserEvent(this.userId, this.context); + + @override + List get props => [userId, context]; +} + +class FilterUsersByRoleEvent extends UserTableEvent { + final List? selectedRoles; + final String? sortOrder; + + const FilterUsersByRoleEvent({this.selectedRoles, this.sortOrder}); + List get props => [selectedRoles, sortOrder]; +} + +class FilterUsersByJobEvent extends UserTableEvent { + final List? selectedJob; + final String? sortOrder; + + const FilterUsersByJobEvent({this.selectedJob, this.sortOrder}); + List get props => [selectedJob, sortOrder]; +} + +class FilterUsersByCreatedEvent extends UserTableEvent { + final List? selectedCreatedBy; + + final String? sortOrder; + + const FilterUsersByCreatedEvent({this.selectedCreatedBy, this.sortOrder}); + List get props => [selectedCreatedBy, sortOrder]; +} + +class FilterUsersByDeActevateEvent extends UserTableEvent { + final List? selectedActivate; + final String? sortOrder; + + const FilterUsersByDeActevateEvent({this.selectedActivate, this.sortOrder}); + List get props => [selectedActivate, sortOrder]; +} + +class FilterOptionsEvent extends UserTableEvent { + final String query; + final List fullOptions; + + FilterOptionsEvent({required this.query, required this.fullOptions}); + + @override + List get props => [query, fullOptions]; +} + +class FilterClearEvent extends UserTableEvent { + FilterClearEvent(); + @override + List get props => []; +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_state.dart b/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_state.dart new file mode 100644 index 00000000..62037315 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/bloc/user_table_state.dart @@ -0,0 +1,91 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; + +sealed class UserTableState extends Equatable { + const UserTableState(); +} + +final class TableInitial extends UserTableState { + @override + List get props => []; +} + +final class RolesLoadingState extends UserTableState { + @override + List get props => []; +} + +final class UsersLoadingState extends UserTableState { + @override + List get props => []; +} + +final class RolesLoadedState extends UserTableState { + @override + List get props => []; +} + +final class UsersLoadedState extends UserTableState { + final List users; + const UsersLoadedState({required this.users}); + @override + List get props => [users]; +} + +final class ErrorState extends UserTableState { + final String message; + + const ErrorState(this.message); + + @override + List get props => [message]; +} + +/// report state +final class SosReportLoadingState extends UserTableState { + @override + List get props => []; +} + +final class RolesErrorState extends UserTableState { + final String message; + + const RolesErrorState(this.message); + + @override + List get props => [message]; +} + +/// automation reports + +final class SosAutomationReportLoadingState extends UserTableState { + @override + List get props => []; +} + +final class SosAutomationReportErrorState extends UserTableState { + final String message; + + const SosAutomationReportErrorState(this.message); + + @override + List get props => [message]; +} + +final class ChangeTapStatus extends UserTableState { + final bool select; + + const ChangeTapStatus({required this.select}); + + @override + List get props => [select]; +} + +class FilterOptionsState extends UserTableState { + final List filteredOptions; + + const FilterOptionsState(this.filteredOptions); + + @override + List get props => [filteredOptions]; +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/creation_date_filter.dart b/lib/pages/roles_and_permission/users_page/users_table/view/creation_date_filter.dart new file mode 100644 index 00000000..45ebc3ae --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/view/creation_date_filter.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +Future showDateFilterMenu({ + required BuildContext context, + Function()? aToZTap, + Function()? zToaTap, + String? isSelected, +}) async { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + overlay.size.width / 2, + 240, + 0, + overlay.size.height, + ), + Offset.zero & overlay.size, + ); + + await showMenu( + context: context, + position: position, + color: ColorsManager.whiteColors, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + items: [ + PopupMenuItem( + onTap: aToZTap, + child: ListTile( + leading: Image.asset( + Assets.AtoZIcon, + width: 25, + ), + title: Text( + "Sort from newest to oldest", + // style: context.textTheme.bodyMedium, + style: TextStyle( + color: isSelected == "NewestToOldest" + ? Colors.black + : Colors.blueGrey), + ), + ), + ), + PopupMenuItem( + onTap: zToaTap, + child: ListTile( + leading: Image.asset( + Assets.ZtoAIcon, + width: 25, + ), + title: Text( + "Sort from oldest to newest", + style: TextStyle( + color: isSelected == "OldestToNewest" + ? Colors.black + : Colors.blueGrey), + ), + ), + ), + ], + ).then((value) { + // setState(() { + // _isDropdownOpen = false; + // }); + }); +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/de_activate_filter.dart b/lib/pages/roles_and_permission/users_page/users_table/view/de_activate_filter.dart new file mode 100644 index 00000000..e78eae6b --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/view/de_activate_filter.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +Future showDeActivateFilterMenu({ + required BuildContext context, + Function()? aToZTap, + Function()? zToaTap, + String? isSelected, +}) async { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + overlay.size.width / 2, + 240, + 0, + overlay.size.height, + ), + Offset.zero & overlay.size, + ); + + await showMenu( + context: context, + position: position, + color: ColorsManager.whiteColors, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + items: [ + PopupMenuItem( + onTap: aToZTap, + child: ListTile( + leading: Image.asset( + Assets.AtoZIcon, + width: 25, + ), + title: Text( + "Sort A to Z", + // style: context.textTheme.bodyMedium, + style: TextStyle( + color: isSelected == "NewestToOldest" + ? Colors.black + : Colors.blueGrey), + ), + ), + ), + PopupMenuItem( + onTap: zToaTap, + child: ListTile( + leading: Image.asset( + Assets.ZtoAIcon, + width: 25, + ), + title: Text( + "Sort Z to A", + style: TextStyle( + color: isSelected == "OldestToNewest" + ? Colors.black + : Colors.blueGrey), + ), + ), + ), + ], + ).then((value) { + // setState(() { + // _isDropdownOpen = false; + // }); + }); +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart b/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart new file mode 100644 index 00000000..e869e10b --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +Future showNameMenu({ + required BuildContext context, + Function()? aToZTap, + Function()? zToaTap, + String? isSelected, +}) async { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + overlay.size.width / 25, + 240, + 0, + overlay.size.height, + ), + Offset.zero & overlay.size, + ); + + await showMenu( + context: context, + position: position, + color: ColorsManager.whiteColors, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + items: [ + PopupMenuItem( + onTap: aToZTap, + child: ListTile( + leading: Image.asset( + Assets.AtoZIcon, + width: 25, + ), + title: Text( + "Sort A to Z", + // style: context.textTheme.bodyMedium, + style: TextStyle( + color: isSelected == "Asc" ? Colors.black : Colors.blueGrey), + ), + ), + ), + PopupMenuItem( + onTap: zToaTap, + child: ListTile( + leading: Image.asset( + Assets.ZtoAIcon, + width: 25, + ), + title: Text( + "Sort Z to A", + style: TextStyle( + color: isSelected == "Desc" ? Colors.black : Colors.blueGrey), + ), + ), + ), + ], + ).then((value) { + // setState(() { + // _isDropdownOpen = false; + // }); + }); +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart b/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart new file mode 100644 index 00000000..92229643 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class DynamicTableScreen extends StatefulWidget { + final List titles; + final List> rows; + final void Function(int columnIndex)? onFilter; + + DynamicTableScreen( + {required this.titles, required this.rows, required this.onFilter}); + + @override + _DynamicTableScreenState createState() => _DynamicTableScreenState(); +} + +class _DynamicTableScreenState extends State + with WidgetsBindingObserver { + late List columnWidths; + late double totalWidth; + + @override + void initState() { + super.initState(); + columnWidths = List.filled(widget.titles.length, 150.0); + totalWidth = columnWidths.reduce((a, b) => a + b); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + final newScreenWidth = MediaQuery.of(context).size.width; + setState(() { + columnWidths = List.generate(widget.titles.length, (index) { + if (index == 1) { + return newScreenWidth * + 0.12; // 20% of screen width for the second column + } else if (index == 9) { + return newScreenWidth * + 0.1; // 25% of screen width for the tenth column + } + return newScreenWidth * + 0.09; // Default to 10% of screen width for other columns + }); + }); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + if (columnWidths.every((width) => width == screenWidth * 7)) { + columnWidths = List.generate(widget.titles.length, (index) { + if (index == 1) { + return screenWidth * 0.11; + } else if (index == 9) { + return screenWidth * 0.1; + } + return screenWidth * 0.09; + }); + setState(() {}); + } + return SingleChildScrollView( + clipBehavior: Clip.none, + scrollDirection: Axis.horizontal, + child: Container( + decoration: containerDecoration.copyWith( + color: ColorsManager.whiteColors, + borderRadius: const BorderRadius.all(Radius.circular(20))), + child: FittedBox( + child: Column( + children: [ + Container( + width: totalWidth, + decoration: containerDecoration.copyWith( + color: ColorsManager.circleRolesBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15))), + child: Row( + children: List.generate(widget.titles.length, (index) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + FittedBox( + child: Container( + padding: const EdgeInsets.only(left: 5, right: 5), + width: columnWidths[index], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + child: Text( + widget.titles[index], + maxLines: 2, + style: const TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w400, + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + if (index != 1 && + index != 9 && + index != 8 && + index != 5) + FittedBox( + child: IconButton( + icon: SvgPicture.asset( + Assets.filterTableIcon, + fit: BoxFit.none, + ), + onPressed: () { + if (widget.onFilter != null) { + widget.onFilter!(index); + } + }, + ), + ) + ], + ), + ), + ), + GestureDetector( + onHorizontalDragUpdate: (details) { + setState(() { + columnWidths[index] = + (columnWidths[index] + details.delta.dx) + .clamp(150.0, 300.0); + totalWidth = columnWidths.reduce((a, b) => a + b); + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.resizeColumn, + child: Container( + color: Colors.green, + child: Container( + color: ColorsManager.boxDivider, + width: 1, + height: 50, + ), + ), + ), + ), + ], + ); + }), + ), + ), + widget.rows.isEmpty + ? SizedBox( + height: MediaQuery.of(context).size.height / 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox( + height: 15, + ), + const Text( + 'No Users', + style: TextStyle( + color: ColorsManager.lightGrayColor, + fontSize: 16, + fontWeight: FontWeight.w700), + ) + ], + ), + ], + ), + ) + : Center( + child: Container( + width: totalWidth, + decoration: containerDecoration.copyWith( + color: ColorsManager.whiteColors, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15))), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.rows.length, + itemBuilder: (context, rowIndex) { + if (columnWidths.every((width) => width == 120.0)) { + columnWidths = List.generate( + widget.titles.length, (index) { + if (index == 1) { + return screenWidth * 0.11; + } else if (index == 9) { + return screenWidth * 0.2; + } + return screenWidth * 0.11; + }); + setState(() {}); + } + final row = widget.rows[rowIndex]; + return Column( + children: [ + Container( + child: Padding( + padding: const EdgeInsets.only( + left: 5, top: 10, right: 5, bottom: 10), + child: Row( + children: + List.generate(row.length, (index) { + return SizedBox( + width: columnWidths[index], + child: SizedBox( + child: Padding( + padding: const EdgeInsets.only( + left: 15, right: 10), + child: row[index], + ), + ), + ); + }), + ), + ), + ), + if (rowIndex < widget.rows.length - 1) + Row( + children: List.generate( + widget.titles.length, (index) { + return SizedBox( + width: columnWidths[index], + child: const Divider( + color: ColorsManager.boxDivider, + thickness: 1, + height: 1, + ), + ); + }), + ), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart new file mode 100644 index 00000000..dae47196 --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -0,0 +1,546 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:number_pagination/number_pagination.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/delete_user_dialog.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/view/popup_menu_filter.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_state.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/view/creation_date_filter.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/view/de_activate_filter.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/view/name_filter.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/view/user_table.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class UsersPage extends StatelessWidget { + UsersPage({super.key}); + + @override + Widget build(BuildContext context) { + final TextEditingController searchController = TextEditingController(); + + Widget actionButton({required String title, required Function()? onTap}) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.only(left: 8, right: 8), + child: Text( + title, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: title == "Delete" + ? ColorsManager.red + : ColorsManager.spaceColor, + fontWeight: FontWeight.w400, + ), + ), + ), + ); + } + + Widget status({required String status}) { + return Center( + child: Container( + padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + color: status == "invited" + ? ColorsManager.invitedOrange.withOpacity(0.5) + : status == "active" + ? ColorsManager.activeGreen.withOpacity(0.5) + : ColorsManager.disabledPink.withOpacity(0.5), + ), + child: Padding( + padding: + const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + status, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: status == "invited" + ? ColorsManager.invitedOrangeText + : status == "active" + ? ColorsManager.activeGreenText + : ColorsManager.disabledRedText, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ); + } + + Widget changeIconStatus( + {required String userId, + required String status, + required Function()? onTap}) { + return Center( + child: InkWell( + onTap: onTap, + child: Padding( + padding: + const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5), + child: SvgPicture.asset( + status == "invited" + ? Assets.invitedIcon + : status == "active" + ? Assets.activeUser + : Assets.deActiveUser, + height: 35, + ), + ), + ), + ); + } + + return BlocBuilder( + builder: (context, state) { + final screenSize = MediaQuery.of(context).size; + final _blocRole = BlocProvider.of(context); + if (state is UsersLoadingState) { + _blocRole.add(ChangePage(_blocRole.currentPage)); + return const Center(child: CircularProgressIndicator()); + } else if (state is UsersLoadedState) { + return Padding( + padding: const EdgeInsets.all(20), + child: Align( + alignment: Alignment.topCenter, + child: ListView( + shrinkWrap: true, + children: [ + Row( + children: [ + Container( + decoration: containerDecoration.copyWith( + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + ), + width: screenSize.width * 0.4, + child: TextFormField( + controller: searchController, + onChanged: (value) { + context + .read() + .add(SearchUsers(value)); + }, + style: const TextStyle(color: Colors.black), + decoration: textBoxDecoration(radios: 15)!.copyWith( + fillColor: ColorsManager.whiteColors, + errorStyle: const TextStyle(height: 0), + hintStyle: context.textTheme.titleSmall?.copyWith( + color: Colors.grey, + fontSize: 12, + ), + hintText: 'Search', + suffixIcon: SizedBox( + child: SvgPicture.asset( + Assets.searchIconUser, + fit: BoxFit.none, + ), + ), + ), + ), + ), + const SizedBox(width: 20), + InkWell( + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const AddNewUserDialog(); + }, + ).then((v) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + }); + }, + child: Container( + decoration: containerWhiteDecoration, + width: screenSize.width * 0.18, + height: 50, + child: const Center( + child: Text( + 'Add New User', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: ColorsManager.blueColor, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 25), + DynamicTableScreen( + onFilter: (columnIndex) { + if (columnIndex == 0) { + showNameMenu( + context: context, + isSelected: _blocRole.currentSortOrder, + aToZTap: () { + context + .read() + .add(const SortUsersByNameAsc()); + }, + zToaTap: () { + context + .read() + .add(const SortUsersByNameDesc()); + }, + ); + } + if (columnIndex == 2) { + final Map checkboxStates = { + for (var item in _blocRole.jobTitle) + item: _blocRole.selectedJobTitles.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 4, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.jobTitle, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortOrder, + onOkPressed: () { + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByJobEvent( + selectedJob: selectedItems, + sortOrder: _blocRole.currentSortOrder, + )); + }, + onSortAtoZ: (v) { + _blocRole.currentSortOrder = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortOrder = v; + }, + ); + } + + if (columnIndex == 3) { + final Map checkboxStates = { + for (var item in _blocRole.roleTypes) + item: _blocRole.selectedRoles.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 4, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.roleTypes, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortOrder, + onOkPressed: () { + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + context.read().add( + FilterUsersByRoleEvent( + selectedRoles: selectedItems, + sortOrder: _blocRole.currentSortOrder)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortOrder = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortOrder = v; + }, + ); + } + if (columnIndex == 4) { + showDateFilterMenu( + context: context, + isSelected: _blocRole.currentSortOrder, + aToZTap: () { + context + .read() + .add(const DateNewestToOldestEvent()); + }, + zToaTap: () { + context + .read() + .add(const DateOldestToNewestEvent()); + }, + ); + } + if (columnIndex == 6) { + final Map checkboxStates = { + for (var item in _blocRole.createdBy) + item: _blocRole.selectedCreatedBy.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 1, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.createdBy, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortOrder, + onOkPressed: () { + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByCreatedEvent( + selectedCreatedBy: selectedItems, + sortOrder: _blocRole.currentSortOrder)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortOrder = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortOrder = v; + }, + ); + } + if (columnIndex == 7) { + final Map checkboxStates = { + for (var item in _blocRole.status) + item: _blocRole.selectedStatuses.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 0, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.status, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortOrder, + onOkPressed: () { + _blocRole.add(FilterClearEvent()); + + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByDeActevateEvent( + selectedActivate: selectedItems, + sortOrder: _blocRole.currentSortOrder)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortOrder = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortOrder = v; + }, + ); + } + if (columnIndex == 8) { + showDeActivateFilterMenu( + context: context, + isSelected: _blocRole.currentSortOrderDate, + aToZTap: () { + context + .read() + .add(const DateNewestToOldestEvent()); + }, + zToaTap: () { + context + .read() + .add(const DateOldestToNewestEvent()); + }, + ); + } + }, + titles: const [ + "Full Name", + "Email Address", + "Job Title", + "Role", + "Creation Date", + "Creation Time", + "Created By", + "Status", + "De/Activate", + "Action" + ], + rows: state.users.map((user) { + return [ + Text('${user.firstName} ${user.lastName}'), + Text(user.email), + Text(user.jobTitle ?? '-'), + Text(user.roleType ?? ''), + Text(user.createdDate ?? ''), + Text(user.createdTime ?? ''), + Text(user.invitedBy), + status( + status: user.isEnabled == false + ? 'disabled' + : user.status, + ), + changeIconStatus( + status: user.isEnabled == false + ? 'disabled' + : user.status, + userId: user.uuid, + onTap: user.status != "invited" + ? () { + // final newStatus = user.status == 'active' + // ? 'disabled' + // : user.status == 'disabled' + // ? 'invited' + // : 'active'; + context.read().add( + ChangeUserStatus( + userId: user.uuid, + newStatus: user.isEnabled == false + ? 'disabled' + : user.status)); + } + : null, + ), + Row( + children: [ + // actionButton( + // title: "Activity Log", + // onTap: () {}, + // ), + actionButton( + title: "Edit", + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog(userId: user.uuid); + }, + ).then((v) { + if (v != null) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + } + }); + }, + ), + actionButton( + title: "Delete", + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return DeleteUserDialog( + onTapDelete: () async { + try { + _blocRole.add(DeleteUserEvent( + user.uuid, context)); + await Future.delayed( + const Duration(seconds: 2)); + return true; + } catch (e) { + return false; + } + }); + }, + ).then((v) { + if (v != null) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + } + }); + }, + ), + ], + ), + ]; + }).toList(), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 500, + child: NumberPagination( + visiblePagesCount: 4, + buttonRadius: 10, + selectedButtonColor: ColorsManager.secondaryColor, + buttonUnSelectedBorderColor: + ColorsManager.grayBorder, + lastPageIcon: + const Icon(Icons.keyboard_double_arrow_right), + firstPageIcon: + const Icon(Icons.keyboard_double_arrow_left), + totalPages: (_blocRole.users.length / + _blocRole.itemsPerPage) + .ceil(), + currentPage: _blocRole.currentPage, + onPageChanged: (int pageNumber) { + _blocRole.currentPage = pageNumber; + context + .read() + .add(ChangePage(pageNumber)); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } else if (state is ErrorState) { + return Center(child: Text(state.message)); + } else { + return const Center(child: Text('No data available.')); + } + }, + ); + } +} diff --git a/lib/pages/roles_and_permission/view/create_role_card.dart b/lib/pages/roles_and_permission/view/create_role_card.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/roles_and_permission/view/role_card.dart b/lib/pages/roles_and_permission/view/role_card.dart new file mode 100644 index 00000000..b08b14ea --- /dev/null +++ b/lib/pages/roles_and_permission/view/role_card.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoleCard extends StatelessWidget { + final String name; + const RoleCard({super.key, required this.name}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, // Card background color + borderRadius: BorderRadius.circular(20), // Rounded corners + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.2), // Shadow color + blurRadius: 20, // Spread of the shadow + offset: const Offset(2, 2), // No directional bias + spreadRadius: 1, // Ensures the shadow is more noticeable + ), + ], + ), + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const CircleAvatar( + backgroundColor: ColorsManager.neutralGray, + radius: 65, + child: CircleAvatar( + backgroundColor: ColorsManager.circleRolesBackground, + radius: 62, + child: Icon( + Icons.admin_panel_settings, + size: 40, + color: Colors.blue, + ), + ), + ), + Text( + name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/roles_and_permission/view/roles_and_permission_page.dart b/lib/pages/roles_and_permission/view/roles_and_permission_page.dart new file mode 100644 index 00000000..44944ed3 --- /dev/null +++ b/lib/pages/roles_and_permission/view/roles_and_permission_page.dart @@ -0,0 +1,94 @@ +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/roles_and_permission/bloc/roles_permission_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/bloc/roles_permission_state.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/bloc/user_table_event.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/users_table/view/users_page.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class RolesAndPermissionPage extends StatelessWidget { + const RolesAndPermissionPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => RolesPermissionBloc(), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + final _blocRole = BlocProvider.of(context); + + return state is RolesLoadingState + ? const Center(child: CircularProgressIndicator()) + : WebScaffold( + enableMenuSidebar: false, + appBarTitle: FittedBox( + child: Text( + 'Roles & Permissions', + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + rightBody: const NavigateHomeGridView(), + centerBody: Row( + children: [ + // TextButton( + // style: TextButton.styleFrom( + // backgroundColor: null, + // ), + // onPressed: () { + // _blocRole.add(const ChangeTapSelected(true)); + // }, + // child: Text( + // 'Roles', + // style: context.textTheme.titleMedium?.copyWith( + // color: (_blocRole.tapSelect == true) + // ? ColorsManager.whiteColors + // : ColorsManager.grayColor, + // fontWeight: (_blocRole.tapSelect == true) + // ? FontWeight.w700 + // : FontWeight.w400, + // ), + // ), + // ), + TextButton( + style: TextButton.styleFrom( + backgroundColor: null, + ), + onPressed: () { + // _blocRole.add(const ChangeTapSelected(false)); + }, + child: Text( + 'Users', + style: context.textTheme.titleMedium?.copyWith( + color: (_blocRole.tapSelect == true) + ? ColorsManager.whiteColors + : ColorsManager.grayColor, + fontWeight: (_blocRole.tapSelect == true) + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ), + ], + ), + scaffoldBody: BlocProvider( + create: (context) => UserTableBloc()..add(const GetUsers()), + child: UsersPage(), + ) + // _blocRole.tapSelect == false + // ? UsersPage( + // blocRole: _blocRole, + // ) + // : RolesPage( + // blocRole: _blocRole, + // ) + ); + }, + ), + ); + } +} diff --git a/lib/pages/roles_and_permission/view/roles_page.dart b/lib/pages/roles_and_permission/view/roles_page.dart new file mode 100644 index 00000000..9c8ef0cd --- /dev/null +++ b/lib/pages/roles_and_permission/view/roles_page.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/roles_and_permission/bloc/roles_permission_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/view/role_card.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RolesPage extends StatelessWidget { + final RolesPermissionBloc blocRole; + const RolesPage({super.key, required this.blocRole}); + + @override + Widget build(BuildContext context) { + final TextEditingController searchController = TextEditingController(); + double screenWidth = MediaQuery.of(context).size.width; + + int crossAxisCount = (screenWidth ~/ 200).clamp(1, 6); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: containerDecoration.copyWith( + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + ), + width: 250, + child: TextFormField( + controller: searchController, + style: const TextStyle(color: Colors.black), + decoration: textBoxDecoration(radios: 15)!.copyWith( + fillColor: ColorsManager.whiteColors, + errorStyle: const TextStyle(height: 0), + hintStyle: context.textTheme.titleSmall?.copyWith( + color: Colors.grey, + fontSize: 12, + ), + hintText: 'Search', + suffixIcon: SvgPicture.asset(Assets.searchIconUser)), + ), + ), + Expanded( + child: GridView.builder( + padding: const EdgeInsets.all(10), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 2 / 2.5, + ), + itemCount: blocRole.roleModel.length ?? 0, + itemBuilder: (context, index) { + final role = blocRole.roleModel[index]; + if (role == null) { + return const SizedBox.shrink(); + } + return RoleCard( + name: role.roleName ?? 'Unknown', + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routines/widgets/dragable_card.dart b/lib/pages/routines/widgets/dragable_card.dart index bc6dae82..77786429 100644 --- a/lib/pages/routines/widgets/dragable_card.dart +++ b/lib/pages/routines/widgets/dragable_card.dart @@ -83,7 +83,7 @@ class DraggableCard extends StatelessWidget { height: 50, width: 50, decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, + color: ColorsManager.circleImageBackground, borderRadius: BorderRadius.circular(90), border: Border.all( color: ColorsManager.graysColor, diff --git a/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart b/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart new file mode 100644 index 00000000..10f3327e --- /dev/null +++ b/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart @@ -0,0 +1,38 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart'; + +class AddDeviceTypeBloc + extends Bloc> { + AddDeviceTypeBloc(List initialProducts) + : super(initialProducts) { + on(_onUpdateProductCount); + } + + void _onUpdateProductCount( + UpdateProductCountEvent event, Emitter> emit) { + final existingProduct = state.firstWhere( + (p) => p.productId == event.productId, + orElse: () => SelectedProduct(productId: event.productId, count: 0,productName: event.productName,product: event.product ), + ); + + if (event.count > 0) { + if (!state.contains(existingProduct)) { + emit([ + ...state, + SelectedProduct(productId: event.productId, count: event.count, productName: event.productName, product: event.product) + ]); + } else { + final updatedList = state.map((p) { + if (p.productId == event.productId) { + return SelectedProduct(productId: p.productId, count: event.count, productName: p.productName,product: p.product); + } + return p; + }).toList(); + emit(updatedList); + } + } else { + emit(state.where((p) => p.productId != event.productId).toList()); + } + } +} diff --git a/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart b/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart new file mode 100644 index 00000000..addb6d67 --- /dev/null +++ b/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; + +abstract class AddDeviceTypeEvent extends Equatable { + @override + List get props => []; +} + +class UpdateProductCountEvent extends AddDeviceTypeEvent { + final String productId; + final int count; + final String productName; + final ProductModel product; + + UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product}); + + @override + List get props => [productId, count]; +} diff --git a/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart b/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart new file mode 100644 index 00000000..9b9d6886 --- /dev/null +++ b/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/action_button_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + + +class AddDeviceTypeWidget extends StatelessWidget { + final List? products; + final ValueChanged>? onProductsSelected; + final List? initialSelectedProducts; + final List? subspaces; + final List? spaceTags; + final List? allTags; + final String spaceName; + final Function(List,List?)? onSave; + + + const AddDeviceTypeWidget( + {super.key, + this.products, + this.initialSelectedProducts, + this.onProductsSelected, + this.subspaces, + this.allTags, + this.spaceTags, + this.onSave, + required this.spaceName}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final crossAxisCount = size.width > 1200 + ? 8 + : size.width > 800 + ? 5 + : 3; + + return BlocProvider( + create: (_) => AddDeviceTypeBloc(initialSelectedProducts ?? []), + child: Builder( + builder: (context) => AlertDialog( + title: const Text('Add Devices'), + backgroundColor: ColorsManager.whiteColors, + content: SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + color: ColorsManager.textFieldGreyColor, + child: Column( + children: [ + const SizedBox(height: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: ScrollableGridViewWidget( + products: products, crossAxisCount: crossAxisCount), + ), + ), + ], + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CancelButton( + label: 'Cancel', + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + ActionButton( + label: 'Continue', + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + onPressed: () async { + final currentState = + context.read().state; + Navigator.of(context).pop(); + + if (currentState.isNotEmpty) { + final initialTags = generateInitialTags( + spaceTags: spaceTags, + subspaces: subspaces, + ); + + final dialogTitle = initialTags.isNotEmpty + ? 'Edit Device' + : 'Assign Tags'; + await showDialog( + barrierDismissible: false, + context: context, + builder: (context) => AssignTagDialog( + products: products, + subspaces: subspaces, + addedProducts: currentState, + allTags: allTags, + spaceName: spaceName, + initialTags: initialTags, + title: dialogTitle, + onSave: (tags,subspaces){ + onSave!(tags,subspaces); + }, + ), + ); + } + }, + ), + ], + ), + ], + ), + )); + } + + List generateInitialTags({ + List? spaceTags, + List? subspaces, + }) { + final List initialTags = []; + + if (spaceTags != null) { + initialTags.addAll(spaceTags); + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + initialTags.addAll( + subspace.tags!.map( + (tag) => tag.copyWith(location: subspace.subspaceName), + ), + ); + } + } + } + + return initialTags; + } +} diff --git a/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart b/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart new file mode 100644 index 00000000..08ad79ac --- /dev/null +++ b/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_name_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DeviceTypeTileWidget extends StatelessWidget { + final ProductModel product; + final List productCounts; + + const DeviceTypeTileWidget({ + super.key, + required this.product, + required this.productCounts, + }); + + @override + Widget build(BuildContext context) { + final selectedProduct = productCounts.firstWhere( + (p) => p.productId == product.uuid, + orElse: () => SelectedProduct( + productId: product.uuid, + count: 0, + productName: product.catName, + product: product), + ); + + return Card( + elevation: 2, + color: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DeviceIconWidget(icon: product.icon ?? Assets.doorSensor), + const SizedBox(height: 4), + DeviceNameWidget(name: product.name), + const SizedBox(height: 4), + CounterWidget( + isCreate: false, + initialCount: selectedProduct.count, + onCountChanged: (newCount) { + context.read().add( + UpdateProductCountEvent( + productId: product.uuid, + count: newCount, + productName: product.catName, + product: product), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart b/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart new file mode 100644 index 00000000..aeee6f1b --- /dev/null +++ b/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; + +class ScrollableGridViewWidget extends StatelessWidget { + final List? products; + final int crossAxisCount; + final List? initialProductCounts; + + const ScrollableGridViewWidget({ + super.key, + required this.products, + required this.crossAxisCount, + this.initialProductCounts, + }); + + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + + return Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: BlocBuilder>( + builder: (context, productCounts) { + return GridView.builder( + controller: scrollController, + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: .8, + ), + itemCount: products?.length ?? 0, + itemBuilder: (context, index) { + final product = products![index]; + final initialProductCount = _findInitialProductCount(product); + + return DeviceTypeTileWidget( + product: product, + productCounts: initialProductCount != null + ? [...productCounts, initialProductCount] + : productCounts, + ); + }, + ); + }, + ), + ); + } + + SelectedProduct? _findInitialProductCount(ProductModel product) { + if (initialProductCounts == null) return null; + final matchingProduct = initialProductCounts!.firstWhere( + (selectedProduct) => selectedProduct.productId == product.uuid, + orElse: () => SelectedProduct( + productId: '', + count: 0, + productName: '', + product: null, + ), + ); + + return matchingProduct.productId.isNotEmpty ? matchingProduct : null; + } +} diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart index ffa5687e..ff584f52 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart @@ -1,20 +1,26 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; import 'package:syncrow_web/services/product_api.dart'; import 'package:syncrow_web/services/space_mana_api.dart'; +import 'package:syncrow_web/services/space_model_mang_api.dart'; class SpaceManagementBloc extends Bloc { final CommunitySpaceManagementApi _api; final ProductApi _productApi; + final SpaceModelManagementApi _spaceModelApi; List? _cachedProducts; - SpaceManagementBloc(this._api, this._productApi) + SpaceManagementBloc(this._api, this._productApi, this._spaceModelApi) : super(SpaceManagementInitial()) { on(_onLoadCommunityAndSpaces); on(_onUpdateSpacePosition); @@ -26,6 +32,8 @@ class SpaceManagementBloc on(_onFetchProducts); on(_onSelectSpace); on(_onNewCommunity); + on(_onBlankState); + on(_onLoadSpaceModel); } void _onUpdateCommunity( @@ -47,10 +55,14 @@ class SpaceManagementBloc break; } } + + var prevSpaceModels = await fetchSpaceModels(previousState); + emit(SpaceManagementLoaded( communities: updatedCommunities, products: previousState.products, selectedCommunity: previousState.selectedCommunity, + spaceModels: prevSpaceModels, )); } } else { @@ -61,6 +73,41 @@ class SpaceManagementBloc } } + Future> fetchSpaceModels( + SpaceManagementState previousState) async { + try { + List allSpaces = []; + List prevSpaceModels = []; + + if (previousState is SpaceManagementLoaded || + previousState is BlankState) { + prevSpaceModels = List.from( + (previousState as dynamic).spaceModels ?? [], + ); + } + + if (prevSpaceModels.isEmpty) { + bool hasNext = true; + int page = 1; + + while (hasNext) { + final spaces = await _spaceModelApi.listSpaceModels(page: page); + if (spaces.isNotEmpty) { + allSpaces.addAll(spaces); + page++; + } else { + hasNext = false; + } + } + prevSpaceModels = await _spaceModelApi.listSpaceModels(page: 1); + } + + return allSpaces; + } catch (e) { + return []; + } + } + void _onloadProducts() async { if (_cachedProducts == null) { final products = await _productApi.fetchProducts(); @@ -84,19 +131,66 @@ class SpaceManagementBloc return await _api.getSpaceHierarchy(communityUuid); } - void _onNewCommunity( + Future _onNewCommunity( NewCommunityEvent event, Emitter emit, - ) { + ) async { try { + final previousState = state; + if (event.communities.isEmpty) { emit(const SpaceManagementError('No communities provided.')); return; } + var prevSpaceModels = await fetchSpaceModels(previousState); + emit(BlankState( communities: event.communities, products: _cachedProducts ?? [], + spaceModels: prevSpaceModels, + )); + } catch (error) { + emit(SpaceManagementError('Error loading communities: $error')); + } + } + + Future _onBlankState( + BlankStateEvent event, Emitter emit) async { + try { + final previousState = state; + var prevSpaceModels = await fetchSpaceModels(previousState); + + if (previousState is SpaceManagementLoaded || + previousState is BlankState) { + final prevCommunities = (previousState as dynamic).communities ?? []; + emit(BlankState( + communities: List.from(prevCommunities), + products: _cachedProducts ?? [], + spaceModels: prevSpaceModels, + )); + return; + } + + final communities = await _api.fetchCommunities(); + final updatedCommunities = + await Future.wait(communities.map((community) async { + final spaces = await _fetchSpacesForCommunity(community.uuid); + return CommunityModel( + uuid: community.uuid, + createdAt: community.createdAt, + updatedAt: community.updatedAt, + name: community.name, + description: community.description, + spaces: spaces, + region: community.region, + ); + })); + + emit(BlankState( + spaceModels: prevSpaceModels, + communities: updatedCommunities, + products: _cachedProducts ?? [], )); } catch (error) { emit(SpaceManagementError('Error loading communities: $error')); @@ -107,6 +201,7 @@ class SpaceManagementBloc LoadCommunityAndSpacesEvent event, Emitter emit, ) async { + var prevState = state; emit(SpaceManagementLoading()); try { _onloadProducts(); @@ -128,8 +223,11 @@ class SpaceManagementBloc }).toList(), ); + final prevSpaceModels = await fetchSpaceModels(prevState); emit(SpaceManagementLoaded( - communities: updatedCommunities, products: _cachedProducts ?? [])); + communities: updatedCommunities, + products: _cachedProducts ?? [], + spaceModels: prevSpaceModels)); } catch (e) { emit(SpaceManagementError('Error loading communities and spaces: $e')); } @@ -169,6 +267,7 @@ class SpaceManagementBloc try { CommunityModel? newCommunity = await _api.createCommunity(event.name, event.description); + var prevSpaceModels = await fetchSpaceModels(previousState); if (newCommunity != null) { if (previousState is SpaceManagementLoaded || @@ -178,6 +277,7 @@ class SpaceManagementBloc ); final updatedCommunities = prevCommunities..add(newCommunity); emit(SpaceManagementLoaded( + spaceModels: prevSpaceModels, communities: updatedCommunities, products: _cachedProducts ?? [], selectedCommunity: newCommunity, @@ -195,11 +295,15 @@ class SpaceManagementBloc SelectCommunityEvent event, Emitter emit, ) async { - _handleCommunitySpaceStateUpdate( - emit: emit, - selectedCommunity: event.selectedCommunity, - selectedSpace: null, - ); + try { + _handleCommunitySpaceStateUpdate( + emit: emit, + selectedCommunity: event.selectedCommunity, + selectedSpace: null, + ); + } catch (e) { + emit(SpaceManagementError('Error updating state: $e')); + } } void _onSelectSpace( @@ -223,16 +327,21 @@ class SpaceManagementBloc try { if (previousState is SpaceManagementLoaded || - previousState is BlankState) { + previousState is BlankState || + previousState is SpaceModelLoaded) { final communities = List.from( (previousState as dynamic).communities, ); + + final spaceModels = List.from( + (previousState as dynamic).spaceModels, + ); emit(SpaceManagementLoaded( - communities: communities, - products: _cachedProducts ?? [], - selectedCommunity: selectedCommunity, - selectedSpace: selectedSpace, - )); + communities: communities, + products: _cachedProducts ?? [], + selectedCommunity: selectedCommunity, + selectedSpace: selectedSpace, + spaceModels: spaceModels ?? [])); } } catch (e) { emit(SpaceManagementError('Error updating state: $e')); @@ -255,7 +364,7 @@ class SpaceManagementBloc emit(SpaceCreationSuccess(spaces: updatedSpaces)); if (previousState is SpaceManagementLoaded) { - _updateLoadedState( + await _updateLoadedState( previousState, allSpaces, event.communityUuid, @@ -273,23 +382,25 @@ class SpaceManagementBloc } } - void _updateLoadedState( + Future _updateLoadedState( SpaceManagementLoaded previousState, List allSpaces, String communityUuid, Emitter emit, - ) { + ) async { + var prevSpaceModels = await fetchSpaceModels(previousState); + final communities = List.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, - )); + communities: communities, + products: _cachedProducts ?? [], + selectedCommunity: community, + selectedSpace: null, + spaceModels: prevSpaceModels)); return; } } @@ -309,34 +420,53 @@ class SpaceManagementBloc await _api.deleteSpace(communityUuid, parent.uuid!); } } catch (e) { - rethrow; // Decide whether to stop execution or continue + rethrow; } } + orderedSpaces.removeWhere((space) => parentsToDelete.contains(space)); for (var space in orderedSpaces) { try { if (space.uuid != null && space.uuid!.isNotEmpty) { final response = await _api.updateSpace( - communityId: communityUuid, - spaceId: space.uuid!, - name: space.name, - parentId: space.parent?.uuid, - isPrivate: space.isPrivate, - position: space.position, - icon: space.icon, - direction: space.incomingConnection?.direction, - products: space.selectedProducts); + communityId: communityUuid, + spaceId: space.uuid!, + name: space.name, + parentId: space.parent?.uuid, + isPrivate: space.isPrivate, + position: space.position, + icon: space.icon, + direction: space.incomingConnection?.direction, + ); } else { // Call create if the space does not have a UUID + final List tagBodyModels = space.tags != null + ? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList() + : []; + + final createSubspaceBodyModels = space.subspaces?.map((subspace) { + final tagBodyModels = subspace.tags + ?.map((tag) => tag.toCreateTagBodyModel()) + .toList() ?? + []; + return CreateSubspaceModel() + ..subspaceName = subspace.subspaceName + ..tags = tagBodyModels; + }).toList() ?? + []; + final response = await _api.createSpace( - communityId: communityUuid, - name: space.name, - parentId: space.parent?.uuid, - isPrivate: space.isPrivate, - position: space.position, - icon: space.icon, - direction: space.incomingConnection?.direction, - products: space.selectedProducts); + communityId: communityUuid, + name: space.name, + parentId: space.parent?.uuid, + isPrivate: space.isPrivate, + position: space.position, + icon: space.icon, + direction: space.incomingConnection?.direction, + spaceModelUuid: space.spaceModel?.uuid, + tags: tagBodyModels, + subspaces: createSubspaceBodyModels, + ); space.uuid = response?.uuid; } } catch (e) { @@ -370,4 +500,39 @@ class SpaceManagementBloc } return result.toList(); // Convert back to a list } + + void _onLoadSpaceModel( + SpaceModelLoadEvent event, Emitter emit) async { + emit(SpaceManagementLoading()); + try { + var prevState = state; + + List communities = await _api.fetchCommunities(); + + List updatedCommunities = await Future.wait( + communities.map((community) async { + List spaces = + await _fetchSpacesForCommunity(community.uuid); + return CommunityModel( + uuid: community.uuid, + createdAt: community.createdAt, + updatedAt: community.updatedAt, + name: community.name, + description: community.description, + spaces: spaces, // New spaces list + region: community.region, + ); + }).toList(), + ); + + var prevSpaceModels = await fetchSpaceModels(prevState); + + emit(SpaceModelLoaded( + communities: updatedCommunities, + products: _cachedProducts ?? [], + spaceModels: prevSpaceModels)); + } catch (e) { + emit(SpaceManagementError('Error loading communities and spaces: $e')); + } + } } diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_event.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_event.dart index 9e3dcc74..d25534b4 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_event.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_event.dart @@ -140,3 +140,8 @@ class LoadSpaceHierarchyEvent extends SpaceManagementEvent { @override List get props => [communityId]; } + + +class BlankStateEvent extends SpaceManagementEvent {} + +class SpaceModelLoadEvent extends SpaceManagementEvent {} diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_state.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_state.dart index eca8c16f..571651e5 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_state.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_state.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; abstract class SpaceManagementState extends Equatable { const SpaceManagementState(); @@ -19,22 +20,27 @@ class SpaceManagementLoaded extends SpaceManagementState { final List products; CommunityModel? selectedCommunity; SpaceModel? selectedSpace; + List? spaceModels; SpaceManagementLoaded( {required this.communities, required this.products, this.selectedCommunity, - this.selectedSpace}); + this.selectedSpace, + this.spaceModels}); +} + +class SpaceModelManagenetLoaded extends SpaceManagementState { + SpaceModelManagenetLoaded(); } class BlankState extends SpaceManagementState { final List communities; final List products; + List? spaceModels; - BlankState({ - required this.communities, - required this.products, - }); + BlankState( + {required this.communities, required this.products, this.spaceModels}); } class SpaceCreationSuccess extends SpaceManagementState { @@ -54,3 +60,18 @@ class SpaceManagementError extends SpaceManagementState { @override List get props => [errorMessage]; } + +class SpaceModelLoaded extends SpaceManagementState { + List spaceModels; + final List products; + final List communities; + + SpaceModelLoaded({ + required this.communities, + required this.products, + required this.spaceModels, + }); + + @override + List get props => [communities, products, spaceModels]; +} diff --git a/lib/pages/spaces_management/all_spaces/model/create_subspace_model.dart b/lib/pages/spaces_management/all_spaces/model/create_subspace_model.dart new file mode 100644 index 00000000..ce480169 --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/model/create_subspace_model.dart @@ -0,0 +1,13 @@ +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; + +class CreateSubspaceModel { + late String subspaceName; + late List? tags; + + Map toJson() { + return { + 'subspaceName': subspaceName, + 'tags': tags?.map((tag) => tag.toJson()).toList(), + }; + } +} diff --git a/lib/pages/spaces_management/all_spaces/model/product_model.dart b/lib/pages/spaces_management/all_spaces/model/product_model.dart index 5a0e92e1..a4ebd550 100644 --- a/lib/pages/spaces_management/all_spaces/model/product_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/product_model.dart @@ -19,7 +19,6 @@ class ProductModel { // Factory method to create a Product from JSON factory ProductModel.fromMap(Map json) { - String icon = _mapIconToProduct(json['prodType']); return ProductModel( uuid: json['uuid'], catName: json['catName'], @@ -67,4 +66,25 @@ class ProductModel { String toString() { return 'ProductModel(uuid: $uuid, catName: $catName, prodId: $prodId, prodType: $prodType, name: $name, icon: $icon)'; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProductModel && + runtimeType == other.runtimeType && + uuid == other.uuid && + catName == other.catName && + prodId == other.prodId && + prodType == other.prodType && + name == other.name && + icon == other.icon; + + @override + int get hashCode => + uuid.hashCode ^ + catName.hashCode ^ + prodId.hashCode ^ + prodType.hashCode ^ + name.hashCode ^ + icon.hashCode; } diff --git a/lib/pages/spaces_management/all_spaces/model/selected_product_model.dart b/lib/pages/spaces_management/all_spaces/model/selected_product_model.dart index 9a06698f..91314e42 100644 --- a/lib/pages/spaces_management/all_spaces/model/selected_product_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/selected_product_model.dart @@ -1,13 +1,18 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; + class SelectedProduct { final String productId; int count; + final String productName; + final ProductModel? product; - SelectedProduct({required this.productId, required this.count}); + SelectedProduct({required this.productId, required this.count, required this.productName, this.product}); Map toJson() { return { 'productId': productId, 'count': count, + 'productName': productName, }; } diff --git a/lib/pages/spaces_management/all_spaces/model/space_model.dart b/lib/pages/spaces_management/all_spaces/model/space_model.dart index df6550f8..c8da9d9e 100644 --- a/lib/pages/spaces_management/all_spaces/model/space_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/space_model.dart @@ -1,11 +1,13 @@ import 'dart:ui'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:uuid/uuid.dart'; -enum SpaceStatus { newSpace, modified, unchanged, deleted } +enum SpaceStatus { newSpace, modified, unchanged, deleted, parentDeleted } class SpaceModel { String? uuid; @@ -20,8 +22,10 @@ class SpaceModel { Offset position; bool isHovered; SpaceStatus status; - List selectedProducts; String internalId; + SpaceTemplateModel? spaceModel; + List? tags; + List? subspaces; List outgoingConnections = []; // Connections from this space Connection? incomingConnection; // Connections to this space @@ -41,7 +45,9 @@ class SpaceModel { this.isHovered = false, this.incomingConnection, this.status = SpaceStatus.unchanged, - this.selectedProducts = const [], + this.spaceModel, + this.tags, + this.subspaces, }) : internalId = internalId ?? const Uuid().v4(); factory SpaceModel.fromJson(Map json, @@ -64,6 +70,11 @@ class SpaceModel { name: json['spaceName'], isPrivate: json['isPrivate'] ?? false, invitationCode: json['invitationCode'], + subspaces: (json['subspaces'] as List?) + ?.where((e) => e is Map) // Validate type + .map((e) => SubspaceModel.fromJson(e as Map)) + .toList() ?? + [], parent: parentInternalId != null ? SpaceModel( internalId: parentInternalId, @@ -85,14 +96,11 @@ class SpaceModel { icon: json['icon'] ?? Assets.location, position: Offset(json['x'] ?? 0, json['y'] ?? 0), isHovered: false, - selectedProducts: json['spaceProducts'] != null - ? (json['spaceProducts'] as List).map((product) { - return SelectedProduct( - productId: product['product']['uuid'], - count: product['productCount'], - ); - }).toList() - : [], + tags: (json['tags'] as List?) + ?.where((item) => item is Map) // Validate type + .map((item) => Tag.fromJson(item as Map)) + .toList() ?? + [], ); if (json['incomingConnections'] != null && @@ -118,6 +126,7 @@ class SpaceModel { 'isPrivate': isPrivate, 'invitationCode': invitationCode, 'parent': parent?.uuid, + 'subspaces': subspaces?.map((e) => e.toJson()).toList(), 'community': community?.toMap(), 'children': children.map((child) => child.toMap()).toList(), 'icon': icon, @@ -125,6 +134,7 @@ class SpaceModel { 'isHovered': isHovered, 'outgoingConnections': outgoingConnections.map((c) => c.toMap()).toList(), 'incomingConnection': incomingConnection?.toMap(), + 'tags': tags?.map((e) => e.toJson()).toList(), }; } @@ -132,3 +142,28 @@ class SpaceModel { outgoingConnections.add(connection); } } + +extension SpaceExtensions on SpaceModel { + List listAllTagValues() { + final List tagValues = []; + + if (tags != null) { + tagValues.addAll( + tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty)); + } + + if (subspaces != null) { + for (final subspace in subspaces!) { + if (subspace.tags != null) { + tagValues.addAll( + subspace.tags! + .map((tag) => tag.tag ?? '') + .where((tag) => tag.isNotEmpty), + ); + } + } + } + + return tagValues; + } +} diff --git a/lib/pages/spaces_management/all_spaces/model/subspace_model.dart b/lib/pages/spaces_management/all_spaces/model/subspace_model.dart new file mode 100644 index 00000000..2c86523f --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/model/subspace_model.dart @@ -0,0 +1,110 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/utils/constants/action_enum.dart'; + +import 'tag.dart'; + +class SubspaceModel { + final String? uuid; + String subspaceName; + final bool disabled; + List? tags; + + SubspaceModel({ + this.uuid, + required this.subspaceName, + required this.disabled, + this.tags, + }); + + factory SubspaceModel.fromJson(Map json) { + return SubspaceModel( + uuid: json['uuid'] ?? '', + subspaceName: json['subspaceName'] ?? '', + disabled: json['disabled'] ?? false, + tags: (json['tags'] as List?) + ?.map((item) => Tag.fromJson(item)) + .toList() ?? + [], + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'subspaceName': subspaceName, + 'disabled': disabled, + 'tags': tags?.map((e) => e.toJson()).toList() ?? [], + }; + } +} + +class UpdateSubspaceModel { + final String uuid; + final Action action; + final String? subspaceName; + final List? tags; + UpdateSubspaceModel({ + required this.action, + required this.uuid, + this.subspaceName, + this.tags, + }); + + factory UpdateSubspaceModel.fromJson(Map json) { + return UpdateSubspaceModel( + action: ActionExtension.fromValue(json['action']), + uuid: json['uuid'] ?? '', + subspaceName: json['subspaceName'] ?? '', + tags: (json['tags'] as List) + .map((item) => UpdateTag.fromJson(item)) + .toList(), + ); + } + + Map toJson() { + return { + 'action': action.value, + 'uuid': uuid, + 'subspaceName': subspaceName, + 'tags': tags?.map((e) => e.toJson()).toList() ?? [], + }; + } +} + +class UpdateTag { + final Action action; + final String? uuid; + final String tag; + final bool disabled; + final ProductModel? product; + + UpdateTag({ + required this.action, + this.uuid, + required this.tag, + required this.disabled, + this.product, + }); + + factory UpdateTag.fromJson(Map json) { + return UpdateTag( + action: ActionExtension.fromValue(json['action']), + uuid: json['uuid'] ?? '', + tag: json['tag'] ?? '', + disabled: json['disabled'] ?? false, + product: json['product'] != null + ? ProductModel.fromMap(json['product']) + : null, + ); + } + + Map toJson() { + return { + 'action': action.value, + 'uuid': uuid, + 'tag': tag, + 'disabled': disabled, + 'product': product?.toMap(), + }; + } +} \ No newline at end of file diff --git a/lib/pages/spaces_management/all_spaces/model/tag.dart b/lib/pages/spaces_management/all_spaces/model/tag.dart new file mode 100644 index 00000000..98494f7f --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/model/tag.dart @@ -0,0 +1,68 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; +import 'package:uuid/uuid.dart'; + +class Tag { + String? uuid; + String? tag; + final ProductModel? product; + String internalId; + String? location; + + Tag( + {this.uuid, + required this.tag, + this.product, + String? internalId, + this.location}) + : internalId = internalId ?? const Uuid().v4(); + + factory Tag.fromJson(Map json) { + final String internalId = json['internalId'] ?? const Uuid().v4(); + + return Tag( + uuid: json['uuid'] ?? '', + internalId: internalId, + tag: json['tag'] ?? '', + product: json['product'] != null + ? ProductModel.fromMap(json['product']) + : null, + ); + } + + Tag copyWith({ + String? tag, + ProductModel? product, + String? location, + }) { + return Tag( + tag: tag ?? this.tag, + product: product ?? this.product, + location: location ?? this.location, + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'tag': tag, + 'product': product?.toMap(), + }; + } +} + +extension TagModelExtensions on Tag { + TagBodyModel toTagBodyModel() { + return TagBodyModel() + ..uuid = uuid ?? '' + ..tag = tag ?? '' + ..productUuid = product?.uuid; + } + + CreateTagBodyModel toCreateTagBodyModel() { + return CreateTagBodyModel() + ..tag = tag ?? '' + ..productUuid = product?.uuid; + } +} diff --git a/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart b/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart index 8cad58b2..e601cca4 100644 --- a/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart +++ b/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart @@ -1,15 +1,15 @@ 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/structure_selector/bloc/center_body_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/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/pages/spaces_management/structure_selector/view/center_body_widget.dart'; import 'package:syncrow_web/services/product_api.dart'; import 'package:syncrow_web/services/space_mana_api.dart'; +import 'package:syncrow_web/services/space_model_mang_api.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; class SpaceManagementPage extends StatefulWidget { @@ -22,48 +22,59 @@ class SpaceManagementPage extends StatefulWidget { class SpaceManagementPageState extends State { final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi(); final ProductApi _productApi = ProductApi(); - Map> communitySpaces = {}; - List products = []; - bool isProductDataLoaded = false; - - @override - void initState() { - super.initState(); - } - + final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi(); @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SpaceManagementBloc(_api, _productApi) - ..add(LoadCommunityAndSpacesEvent()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => SpaceManagementBloc(_api, _productApi, _spaceModelApi) + ..add(LoadCommunityAndSpacesEvent()), + ), + BlocProvider( + create: (_) => CenterBodyBloc(), + ), + ], child: WebScaffold( appBarTitle: Text('Space Management', style: Theme.of(context).textTheme.headlineLarge), enableMenuSidebar: false, + centerBody: CenterBodyWidget(), rightBody: const NavigateHomeGridView(), scaffoldBody: BlocBuilder( - 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(); - }), + 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, + shouldNavigateToSpaceModelPage: false, + ); + } else if (state is SpaceManagementLoaded) { + return LoadedSpaceView( + communities: state.communities, + selectedCommunity: state.selectedCommunity, + selectedSpace: state.selectedSpace, + products: state.products, + spaceModels: state.spaceModels, + shouldNavigateToSpaceModelPage: false, + ); + } else if (state is SpaceModelLoaded) { + return LoadedSpaceView( + communities: state.communities, + products: state.products, + spaceModels: state.spaceModels, + shouldNavigateToSpaceModelPage: true, + ); + } else if (state is SpaceManagementError) { + return Center(child: Text('Error: ${state.errorMessage}')); + } + return Container(); + }, + ), ), ); } diff --git a/lib/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart index 40759b58..0e9f4bd1 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart @@ -99,7 +99,7 @@ class _AddDeviceWidgetState extends State { _buildActionButton('Cancel', ColorsManager.boxColor, ColorsManager.blackColor, () { Navigator.of(context).pop(); }), - _buildActionButton('Continue', ColorsManager.secondaryColor, Colors.white, () { + _buildActionButton('Continue', ColorsManager.secondaryColor, ColorsManager.whiteColors, () { Navigator.of(context).pop(); if (widget.onProductsSelected != null) { widget.onProductsSelected!(productCounts); @@ -114,7 +114,7 @@ class _AddDeviceWidgetState extends State { Widget _buildDeviceTypeTile(ProductModel product, Size size) { final selectedProduct = productCounts.firstWhere( (p) => p.productId == product.uuid, - orElse: () => SelectedProduct(productId: product.uuid, count: 0), + orElse: () => SelectedProduct(productId: product.uuid, count: 0, productName: product.catName, product: product), ); return SizedBox( @@ -137,13 +137,14 @@ class _AddDeviceWidgetState extends State { _buildDeviceName(product, size), const SizedBox(height: 4), CounterWidget( + isCreate: false, initialCount: selectedProduct.count, onCountChanged: (newCount) { setState(() { if (newCount > 0) { if (!productCounts.contains(selectedProduct)) { productCounts - .add(SelectedProduct(productId: product.uuid, count: newCount)); + .add(SelectedProduct(productId: product.uuid, count: newCount, productName: product.catName, product: product)); } else { selectedProduct.count = newCount; } diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart index 63306581..02d3819a 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -17,6 +18,7 @@ class CommunityStructureHeader extends StatefulWidget { final ValueChanged onNameSubmitted; final List communities; final CommunityModel? community; + final SpaceModel? selectedSpace; const CommunityStructureHeader( {super.key, @@ -29,7 +31,8 @@ class CommunityStructureHeader extends StatefulWidget { required this.onEditName, required this.onNameSubmitted, this.community, - required this.communities}); + required this.communities, + this.selectedSpace}); @override State createState() => @@ -137,10 +140,8 @@ class _CommunityStructureHeaderState extends State { ], ), ), - if (widget.isSave) ...[ - const SizedBox(width: 8), - _buildActionButtons(theme), - ], + const SizedBox(width: 8), + _buildActionButtons(theme), ], ), ], @@ -152,11 +153,19 @@ class _CommunityStructureHeaderState extends State { alignment: WrapAlignment.end, spacing: 10, children: [ + if (widget.isSave) + _buildButton( + label: "Save", + icon: const Icon(Icons.save, + size: 18, color: ColorsManager.spaceColor), + onPressed: widget.onSave, + theme: theme), + if(widget.selectedSpace!= null) _buildButton( - label: "Save", - icon: const Icon(Icons.save, - size: 18, color: ColorsManager.spaceColor), - onPressed: widget.onSave, + label: "Delete", + icon: const Icon(Icons.delete, + size: 18, color: ColorsManager.warningRed), + onPressed: widget.onDelete, theme: theme), ], ); @@ -178,7 +187,7 @@ class _CommunityStructureHeaderState extends State { padding: 2.0, height: buttonHeight, elevation: 0, - borderColor: Colors.grey.shade300, + borderColor: ColorsManager.lightGrayColor, child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart index c11563da..f569d252 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart @@ -11,12 +11,15 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CommunityStructureArea extends StatefulWidget { @@ -26,6 +29,7 @@ class CommunityStructureArea extends StatefulWidget { final ValueChanged? onSpaceSelected; final List communities; final List spaces; + final List? spaceModels; CommunityStructureArea({ this.selectedCommunity, @@ -34,6 +38,7 @@ class CommunityStructureArea extends StatefulWidget { this.products, required this.spaces, this.onSpaceSelected, + this.spaceModels, }); @override @@ -126,6 +131,7 @@ class _CommunityStructureAreaState extends State { isEditingName: isEditingName, nameController: _nameController, onSave: _saveSpaces, + selectedSpace: widget.selectedSpace, onDelete: _onDelete, onEditName: () { setState(() { @@ -171,7 +177,8 @@ class _CommunityStructureAreaState extends State { painter: CurvedLinePainter([connection])), ), for (var entry in spaces.asMap().entries) - if (entry.value.status != SpaceStatus.deleted) + if (entry.value.status != SpaceStatus.deleted && + entry.value.status != SpaceStatus.parentDeleted) Positioned( left: entry.value.position.dx, top: entry.value.position.dy, @@ -284,12 +291,16 @@ class _CommunityStructureAreaState extends State { builder: (BuildContext context) { return CreateSpaceDialog( products: widget.products, + spaceModels: widget.spaceModels, parentSpace: parentIndex != null ? spaces[parentIndex] : null, - onCreateSpace: (String name, String icon, - List selectedProducts) { + onCreateSpace: (String name, + String icon, + List selectedProducts, + SpaceTemplateModel? spaceModel, + List? subspaces, + List? tags) { setState(() { // Set the first space in the center or use passed position - Offset centerPosition = position ?? _getCenterPosition(screenSize); SpaceModel newSpace = SpaceModel( @@ -299,7 +310,9 @@ class _CommunityStructureAreaState extends State { isPrivate: false, children: [], status: SpaceStatus.newSpace, - selectedProducts: selectedProducts); + spaceModel: spaceModel, + subspaces: subspaces, + tags: tags); if (parentIndex != null && direction != null) { SpaceModel parentSpace = spaces[parentIndex]; @@ -335,14 +348,19 @@ class _CommunityStructureAreaState extends State { icon: space.icon, editSpace: space, isEdit: true, - selectedProducts: space.selectedProducts, - onCreateSpace: (String name, String icon, - List selectedProducts) { + onCreateSpace: (String name, + String icon, + List selectedProducts, + SpaceTemplateModel? spaceModel, + List? subspaces, + List? tags) { setState(() { // Update the space's properties space.name = name; space.icon = icon; - space.selectedProducts = selectedProducts; + space.spaceModel = spaceModel; + space.subspaces = subspaces; + space.tags = tags; if (space.status != SpaceStatus.newSpace) { space.status = SpaceStatus.modified; // Mark as modified @@ -365,10 +383,11 @@ class _CommunityStructureAreaState extends State { List result = []; void flatten(SpaceModel space) { - if (space.status == SpaceStatus.deleted) return; - + if (space.status == SpaceStatus.deleted || + space.status == SpaceStatus.parentDeleted) { + return; + } result.add(space); - for (var child in space.children) { flatten(child); } @@ -436,21 +455,15 @@ class _CommunityStructureAreaState extends State { } void _onDelete() { - if (widget.selectedCommunity != null && - widget.selectedCommunity?.uuid != null && - widget.selectedSpace == null) { - context.read().add(DeleteCommunityEvent( - communityUuid: widget.selectedCommunity!.uuid, - )); - } if (widget.selectedSpace != null) { setState(() { for (var space in spaces) { - if (space.uuid == widget.selectedSpace?.uuid) { + if (space.internalId == widget.selectedSpace?.internalId) { space.status = SpaceStatus.deleted; _markChildrenAsDeleted(space); } } + _removeConnectionsForDeletedSpaces(); }); } @@ -458,7 +471,8 @@ class _CommunityStructureAreaState extends State { void _markChildrenAsDeleted(SpaceModel parent) { for (var child in parent.children) { - child.status = SpaceStatus.deleted; + child.status = SpaceStatus.parentDeleted; + _markChildrenAsDeleted(child); } } @@ -466,7 +480,9 @@ class _CommunityStructureAreaState extends State { void _removeConnectionsForDeletedSpaces() { connections.removeWhere((connection) { return connection.startSpace.status == SpaceStatus.deleted || - connection.endSpace.status == SpaceStatus.deleted; + connection.endSpace.status == SpaceStatus.deleted || + connection.startSpace.status == SpaceStatus.parentDeleted || + connection.endSpace.status == SpaceStatus.parentDeleted; }); } diff --git a/lib/pages/spaces_management/all_spaces/widgets/counter_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/counter_widget.dart index 66935b12..2289819b 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/counter_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/counter_widget.dart @@ -4,12 +4,14 @@ import 'package:syncrow_web/utils/color_manager.dart'; class CounterWidget extends StatefulWidget { final int initialCount; final ValueChanged onCountChanged; + final bool isCreate; - const CounterWidget({ - Key? key, - this.initialCount = 0, - required this.onCountChanged, - }) : super(key: key); + const CounterWidget( + {Key? key, + this.initialCount = 0, + required this.onCountChanged, + required this.isCreate}) + : super(key: key); @override State createState() => _CounterWidgetState(); @@ -53,25 +55,26 @@ class _CounterWidgetState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildCounterButton(Icons.remove, _decrementCounter), + _buildCounterButton(Icons.remove, _decrementCounter,!widget.isCreate ), const SizedBox(width: 8), Text( '$_counter', - style: theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.spaceColor), + style: theme.textTheme.bodyLarge + ?.copyWith(color: ColorsManager.spaceColor), ), const SizedBox(width: 8), - _buildCounterButton(Icons.add, _incrementCounter), + _buildCounterButton(Icons.add, _incrementCounter, false), ], ), ); } - Widget _buildCounterButton(IconData icon, VoidCallback onPressed) { + Widget _buildCounterButton(IconData icon, VoidCallback onPressed, bool isDisabled) { return GestureDetector( - onTap: onPressed, + onTap: isDisabled? null: onPressed, child: Icon( icon, - color: ColorsManager.spaceColor, + color: isDisabled? ColorsManager.spaceColor.withOpacity(0.3): ColorsManager.spaceColor, size: 18, ), ); diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart index f6a02833..6f3e9fcb 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart @@ -2,19 +2,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/space_icon_const.dart'; class CreateSpaceDialog extends StatefulWidget { - final Function(String, String, List selectedProducts) - onCreateSpace; + final Function(String, String, List selectedProducts, + SpaceTemplateModel? spaceModel, List? subspaces, List? tags) onCreateSpace; final List? products; final String? name; final String? icon; @@ -22,6 +26,9 @@ class CreateSpaceDialog extends StatefulWidget { final List selectedProducts; final SpaceModel? parentSpace; final SpaceModel? editSpace; + final List? spaceModels; + final List? subspaces; + final List? tags; const CreateSpaceDialog( {super.key, @@ -32,7 +39,10 @@ class CreateSpaceDialog extends StatefulWidget { this.icon, this.isEdit = false, this.editSpace, - this.selectedProducts = const []}); + this.selectedProducts = const [], + this.spaceModels, + this.subspaces, + this.tags}); @override CreateSpaceDialogState createState() => CreateSpaceDialogState(); @@ -40,12 +50,15 @@ class CreateSpaceDialog extends StatefulWidget { class CreateSpaceDialogState extends State { String selectedIcon = Assets.location; + SpaceTemplateModel? selectedSpaceModel; String enteredName = ''; List selectedProducts = []; late TextEditingController nameController; bool isOkButtonEnabled = false; bool isNameFieldInvalid = false; bool isNameFieldExist = false; + List? subspaces; + List? tags; @override void initState() { @@ -58,196 +71,485 @@ class CreateSpaceDialogState extends State { enteredName.isNotEmpty || nameController.text.isNotEmpty; } - @override @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; - return AlertDialog( title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'), backgroundColor: ColorsManager.whiteColors, content: SizedBox( - width: screenWidth * 0.5, // Limit dialog width + width: screenWidth * 0.5, child: SingleChildScrollView( - // Scrollable content to prevent overflow - child: Column( - mainAxisSize: MainAxisSize.min, + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - alignment: Alignment.center, - children: [ - Container( - width: screenWidth * 0.1, // Adjusted width - height: screenWidth * 0.1, // Adjusted height - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - shape: BoxShape.circle, - ), - ), - SvgPicture.asset( - selectedIcon, - width: screenWidth * 0.04, - height: screenWidth * 0.04, - ), - Positioned( - top: 6, - right: 6, - child: InkWell( - onTap: _showIconSelectionDialog, - child: Container( - width: screenWidth * 0.020, - height: screenWidth * 0.020, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: SvgPicture.asset( - Assets.iconEdit, - width: screenWidth * 0.06, - height: screenWidth * 0.06, - ), - ), - ), - ), - ], - ), - const SizedBox(width: 16), - Expanded( - // Ensure the text field expands responsively - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Stack( + alignment: Alignment.center, children: [ - TextField( - controller: nameController, - onChanged: (value) { - enteredName = value.trim(); - setState(() { - isNameFieldExist = false; - isOkButtonEnabled = false; - isNameFieldInvalid = value.isEmpty; - - if (!isNameFieldInvalid) { - if (_isNameConflict(value)) { - isNameFieldExist = true; - isOkButtonEnabled = false; - } else { - isNameFieldExist = false; - isOkButtonEnabled = true; - } - } - }); - }, - style: const TextStyle(color: Colors.black), - decoration: InputDecoration( - hintText: 'Please enter the name', - hintStyle: const TextStyle( - fontSize: 14, - color: ColorsManager.lightGrayColor, - fontWeight: FontWeight.w400, - ), - filled: true, - fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: isNameFieldInvalid || isNameFieldExist - ? ColorsManager.red - : ColorsManager.boxColor, - width: 1.5, + Container( + width: screenWidth * 0.1, + height: screenWidth * 0.1, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + ), + SvgPicture.asset( + selectedIcon, + width: screenWidth * 0.04, + height: screenWidth * 0.04, + ), + Positioned( + top: 20, + right: 20, + child: InkWell( + onTap: _showIconSelectionDialog, + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - width: 1.5, + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, ), ), ), ), - if (isNameFieldInvalid) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Space name should not be empty.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - if (isNameFieldExist) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name already exist', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - const SizedBox(height: 16), - if (selectedProducts.isNotEmpty) - _buildSelectedProductsButtons(widget.products ?? []) - else - DefaultButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AddDeviceWidget( - products: widget.products, - onProductsSelected: (selectedProductsMap) { - setState(() { - selectedProducts = selectedProductsMap; - }); - }, - ), - ); - }, - backgroundColor: ColorsManager.textFieldGreyColor, - foregroundColor: Colors.black, - borderColor: ColorsManager.neutralGray, - borderRadius: 16.0, - padding: 10.0, // Reduced padding for smaller size - child: Align( - alignment: Alignment.centerLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 6.0), - child: SvgPicture.asset( - Assets.addIcon, - width: screenWidth * - 0.015, // Adjust icon size - height: screenWidth * 0.015, - ), - ), - const SizedBox(width: 3), - Flexible( - child: Text( - 'Add devices / Assign a space model', - overflow: TextOverflow - .ellipsis, // Prevent overflow - style: Theme.of(context) - .textTheme - .bodyMedium, - ), - ), - ], - ), - )), ], ), - ), - ], + ], + ), + ), + const SizedBox(width: 20), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: nameController, + onChanged: (value) { + enteredName = value.trim(); + setState(() { + isNameFieldExist = false; + isOkButtonEnabled = false; + isNameFieldInvalid = value.isEmpty; + + if (!isNameFieldInvalid) { + if (_isNameConflict(value)) { + isNameFieldExist = true; + isOkButtonEnabled = false; + } else { + isNameFieldExist = false; + isOkButtonEnabled = true; + } + } + }); + }, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: const TextStyle( + fontSize: 14, + color: ColorsManager.lightGrayColor, + fontWeight: FontWeight.w400, + ), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: isNameFieldInvalid || isNameFieldExist + ? ColorsManager.red + : ColorsManager.boxColor, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + if (isNameFieldInvalid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Space name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameFieldExist) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exist', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + const SizedBox(height: 10), + selectedSpaceModel == null + ? DefaultButton( + onPressed: () { + _showLinkSpaceModelDialog(context); + }, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: Colors.black, + borderColor: ColorsManager.neutralGray, + borderRadius: 16.0, + padding: 10.0, // Reduced padding for smaller size + child: Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + Assets.link, + width: screenWidth * + 0.015, // Adjust icon size + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 3), + Flexible( + child: Text( + 'Link a space model', + overflow: TextOverflow + .ellipsis, // Prevent overflow + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ), + ], + ), + ), + ) + : Container( + width: screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 16.0), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + Chip( + label: Text( + selectedSpaceModel?.modelName ?? '', + style: const TextStyle( + color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide( + color: ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const Icon( + Icons.close, + size: 16, + color: ColorsManager.lightGrayColor, + ), + ), + onDeleted: () => setState(() { + this.selectedSpaceModel = null; + })), + ], + ), + ), + const SizedBox(height: 25), + const Row( + children: [ + Expanded( + child: Divider( + color: ColorsManager.neutralGray, + thickness: 1.0, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + 'OR', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: Divider( + color: ColorsManager.neutralGray, + thickness: 1.0, + ), + ), + ], + ), + const SizedBox(height: 25), + subspaces == null + ? DefaultButton( + onPressed: () { + _showSubSpaceDialog(context, enteredName, [], + false, widget.products, subspaces); + }, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: Colors.black, + borderColor: ColorsManager.neutralGray, + borderRadius: 16.0, + padding: 10.0, // Reduced padding for smaller size + child: Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + Assets.addIcon, + width: screenWidth * + 0.015, // Adjust icon size + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 3), + Flexible( + child: Text( + 'Create sub space', + overflow: TextOverflow + .ellipsis, // Prevent overflow + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ), + ], + ), + ), + ) + : SizedBox( + width: screenWidth * 0.35, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + if (subspaces != null) + ...subspaces!.map( + (subspace) => Chip( + label: Text( + subspace.subspaceName, + style: const TextStyle( + color: ColorsManager + .spaceColor), // Text color + ), + backgroundColor: ColorsManager + .whiteColors, // Chip background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 16), // Rounded chip + side: const BorderSide( + color: ColorsManager + .spaceColor), // Border color + ), + ), + ), + GestureDetector( + onTap: () async { + _showSubSpaceDialog( + context, + enteredName, + [], + false, + widget.products, + subspaces); + }, + child: Chip( + label: const Text( + 'Edit', + style: TextStyle( + color: ColorsManager.spaceColor), + ), + backgroundColor: + ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + (tags?.isNotEmpty == true || + subspaces?.any((subspace) => + subspace.tags?.isNotEmpty == true) == + true) + ? SizedBox( + width: screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + ..._groupTags([ + ...?tags, + ...?subspaces?.expand( + (subspace) => subspace.tags ?? []) + ]).entries.map( + (entry) => Chip( + avatar: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + entry.key.icon ?? + 'assets/icons/gateway.svg', + fit: BoxFit.contain, + ), + ), + label: Text( + 'x${entry.value}', // Show count + style: const TextStyle( + color: ColorsManager.spaceColor, + ), + ), + backgroundColor: + ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor, + ), + ), + ), + ), + GestureDetector( + onTap: () async { + _showTagCreateDialog(context, enteredName, + widget.products); + // Edit action + }, + child: Chip( + label: const Text( + 'Edit', + style: TextStyle( + color: ColorsManager.spaceColor), + ), + backgroundColor: + ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor), + ), + ), + ), + ], + ), + ), + ) + : DefaultButton( + onPressed: () { + _showTagCreateDialog( + context, enteredName, widget.products); + }, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: Colors.black, + borderColor: ColorsManager.neutralGray, + borderRadius: 16.0, + padding: 10.0, // Reduced padding for smaller size + child: Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + Assets.addIcon, + width: screenWidth * + 0.015, // Adjust icon size + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 3), + Flexible( + child: Text( + 'Add devices', + overflow: TextOverflow + .ellipsis, // Prevent overflow + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ), + ], + ), + ), + ) + ], + ), ), ], ), @@ -277,8 +579,8 @@ class CreateSpaceDialogState extends State { ? enteredName : (widget.name ?? ''); if (newName.isNotEmpty) { - widget.onCreateSpace( - newName, selectedIcon, selectedProducts); + widget.onCreateSpace(newName, selectedIcon, + selectedProducts, selectedSpaceModel,subspaces,tags); Navigator.of(context).pop(); } } @@ -313,74 +615,6 @@ class CreateSpaceDialogState extends State { ); } - Widget _buildSelectedProductsButtons(List products) { - final screenWidth = MediaQuery.of(context).size.width; - - return Container( - width: screenWidth * 0.6, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: ColorsManager.neutralGray, - width: 2, - ), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (var i = 0; i < selectedProducts.length; i++) ...[ - HoverableButton( - iconPath: - _mapIconToProduct(selectedProducts[i].productId, products), - text: 'x${selectedProducts[i].count}', - onTap: () { - setState(() { - selectedProducts.remove(selectedProducts[i]); - }); - // Handle button tap - }, - ), - if (i < selectedProducts.length - 1) - const SizedBox( - width: 2), // Add space except after the last button - ], - const SizedBox(width: 2), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => AddDeviceWidget( - products: widget.products, - initialSelectedProducts: selectedProducts, - onProductsSelected: (selectedProductsMap) { - setState(() { - selectedProducts = selectedProductsMap; - }); - }, - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(16), - ), - child: const Icon( - Icons.add, - color: ColorsManager.spaceColor, - size: 24, - ), - ), - ), - ], - ), - ); - } - bool _isNameConflict(String value) { return (widget.parentSpace?.children.any((child) => child.name == value) ?? false) || @@ -390,20 +624,133 @@ class CreateSpaceDialogState extends State { false); } - String _mapIconToProduct(String uuid, List products) { - // Find the product with the matching UUID - final product = products.firstWhere( - (product) => product.uuid == uuid, - orElse: () => ProductModel( - uuid: '', - catName: '', - prodId: '', - prodType: '', - name: '', - icon: Assets.presenceSensor, - ), + void _showLinkSpaceModelDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return LinkSpaceModelDialog( + spaceModels: widget.spaceModels ?? [], + onSave: (selectedModel) { + if (selectedModel != null) { + setState(() { + selectedSpaceModel = selectedModel; + subspaces = null; + }); + } + }, + ); + }, ); + } - return product.icon ?? Assets.presenceSensor; + void _showSubSpaceDialog( + BuildContext context, + String name, + final List? spaceTags, + bool isEdit, + List? products, + final List? existingSubSpaces) { + showDialog( + context: context, + builder: (BuildContext context) { + return CreateSubSpaceDialog( + spaceName: name, + dialogTitle: 'Create Sub-space', + spaceTags: spaceTags, + isEdit: isEdit, + products: products, + existingSubSpaces: existingSubSpaces, + onSave: (slectedSubspaces) { + if (slectedSubspaces != null) { + setState(() { + subspaces = slectedSubspaces; + selectedSpaceModel = null; + }); + } + }); + }, + ); + } + + void _showTagCreateDialog( + BuildContext context, String name, List? products) { + showDialog( + context: context, + builder: (BuildContext context) { + return AddDeviceTypeWidget( + spaceName: name, + products: products, + subspaces: subspaces, + spaceTags: tags, + allTags: [], + initialSelectedProducts: + createInitialSelectedProducts(tags, subspaces), + onSave: (selectedSpaceTags, selectedSubspaces) { + setState(() { + tags = selectedSpaceTags; + selectedSpaceModel = null; + + if (selectedSubspaces != null) { + if (subspaces != null) { + for (final subspace in subspaces!) { + for (final selectedSubspace in selectedSubspaces) { + if (subspace.subspaceName == + selectedSubspace.subspaceName) { + subspace.tags = selectedSubspace.tags; + } + } + } + } + } + }); + }, + ); + }, + ); + } + + List createInitialSelectedProducts( + List? tags, List? subspaces) { + final Map productCounts = {}; + + if (tags != null) { + for (var tag in tags) { + if (tag.product != null) { + productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1; + } + } + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + for (var tag in subspace.tags!) { + if (tag.product != null) { + productCounts[tag.product!] = + (productCounts[tag.product!] ?? 0) + 1; + } + } + } + } + } + + return productCounts.entries + .map((entry) => SelectedProduct( + productId: entry.key.uuid, + count: entry.value, + productName: entry.key.name ?? 'Unnamed', + product: entry.key, + )) + .toList(); + } + + Map _groupTags(List tags) { + final Map groupedTags = {}; + for (var tag in tags) { + if (tag.product != null) { + groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1; + } + } + return groupedTags; } } diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/delete_dialogue.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/delete_dialogue.dart index 8607f9e0..27275be1 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/dialogs/delete_dialogue.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/delete_dialogue.dart @@ -40,8 +40,8 @@ void showDeleteConfirmationDialog(BuildContext context, VoidCallback onConfirm, Navigator.of(context).pop(); // Close the first dialog showProcessingPopup(context, isSpace, onConfirm); }, - style: _dialogButtonStyle(Colors.blue), - child: const Text('Continue', style: TextStyle(color: Colors.white)), + style: _dialogButtonStyle(ColorsManager.spaceColor), + child: const Text('Continue', style: TextStyle(color: ColorsManager.whiteColors)), ), ], ), @@ -83,7 +83,7 @@ void showProcessingPopup(BuildContext context, bool isSpace, VoidCallback onDele ElevatedButton( onPressed: onDelete, style: _dialogButtonStyle(ColorsManager.warningRed), - child: const Text('Delete', style: TextStyle(color: Colors.white)), + child: const Text('Delete', style: TextStyle(color: ColorsManager.whiteColors)), ), CancelButton( label: 'Cancel', @@ -108,7 +108,7 @@ Widget _buildWarningIcon() { color: ColorsManager.warningRed, shape: BoxShape.circle, ), - child: const Icon(Icons.close, color: Colors.white, size: 40), + child: const Icon(Icons.close, color: ColorsManager.whiteColors, size: 40), ); } diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart index 5251ba32..b2a01988 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart @@ -28,7 +28,7 @@ class IconSelectionDialog extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), // Shadow color + color: ColorsManager.blackColor.withOpacity(0.2), // Shadow color blurRadius: 20, // Spread of the blur offset: const Offset(0, 8), // Offset of the shadow ), diff --git a/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart index 7ce56914..cdba0a5a 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart @@ -1,16 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/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'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/view/space_model_page.dart'; +import 'package:syncrow_web/services/space_model_mang_api.dart'; -class LoadedSpaceView extends StatefulWidget { +class LoadedSpaceView extends StatelessWidget { final List communities; final CommunityModel? selectedCommunity; final SpaceModel? selectedSpace; final List? products; + final List? spaceModels; + final bool shouldNavigateToSpaceModelPage; const LoadedSpaceView({ super.key, @@ -18,33 +25,43 @@ class LoadedSpaceView extends StatefulWidget { this.selectedCommunity, this.selectedSpace, this.products, + this.spaceModels, + required this.shouldNavigateToSpaceModelPage }); - @override - _LoadedStateViewState createState() => _LoadedStateViewState(); -} - -class _LoadedStateViewState extends State { @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, + communities: communities, + selectedSpaceUuid: + selectedSpace?.uuid ?? selectedCommunity?.uuid ?? '', ), + shouldNavigateToSpaceModelPage + ? Expanded( + child: BlocProvider( + create: (context) => SpaceModelBloc( + api: SpaceModelManagementApi(), + initialSpaceModels: spaceModels ?? [], + ), + child: SpaceModelPage( + products: products, + ), + ), + ) + : CommunityStructureArea( + selectedCommunity: selectedCommunity, + selectedSpace: selectedSpace, + spaces: selectedCommunity?.spaces ?? [], + products: products, + communities: communities, + spaceModels: spaceModels, + ), ], ), const GradientCanvasBorderWidget(), diff --git a/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart index b077ac9d..40be7284 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart @@ -45,7 +45,7 @@ class PlusButtonWidget extends StatelessWidget { color: ColorsManager.spaceColor, shape: BoxShape.circle, ), - child: const Icon(Icons.add, color: Colors.white, size: 20), + child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20), ), ), ); diff --git a/lib/pages/spaces_management/all_spaces/widgets/selected_products_button_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/selected_products_button_widget.dart new file mode 100644 index 00000000..7076a580 --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/selected_products_button_widget.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SelectedProductsButtons extends StatelessWidget { + final List products; + final List selectedProducts; + final Function(List) onProductsUpdated; + final BuildContext context; + + const SelectedProductsButtons({ + Key? key, + required this.products, + required this.selectedProducts, + required this.onProductsUpdated, + required this.context, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Container( + width: screenWidth * 0.6, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: ColorsManager.neutralGray, + width: 2, + ), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ..._buildSelectedProductButtons(), + _buildAddButton(), + ], + ), + ); + } + + List _buildSelectedProductButtons() { + return [ + for (var i = 0; i < selectedProducts.length; i++) ...[ + HoverableButton( + iconPath: _mapIconToProduct(selectedProducts[i].productId, products), + text: 'x${selectedProducts[i].count}', + onTap: () { + _removeProduct(i); + }, + ), + if (i < selectedProducts.length - 1) + const SizedBox(width: 2), // Add space except after the last button + ], + ]; + } + + Widget _buildAddButton() { + return GestureDetector( + onTap: _showAddDeviceDialog, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.add, + color: ColorsManager.spaceColor, + size: 24, + ), + ), + ); + } + + void _showAddDeviceDialog() { + showDialog( + context: context, + builder: (context) => AddDeviceWidget( + products: products, + initialSelectedProducts: selectedProducts, + onProductsSelected: (selectedProductsMap) { + onProductsUpdated(selectedProductsMap); + }, + ), + ); + } + + void _removeProduct(int index) { + final updatedProducts = [...selectedProducts]; + updatedProducts.removeAt(index); + onProductsUpdated(updatedProducts); + } + + String _mapIconToProduct(String uuid, List products) { + // Find the product with the matching UUID + final product = products.firstWhere( + (product) => product.uuid == uuid, + orElse: () => ProductModel( + uuid: '', + catName: '', + prodId: '', + prodType: '', + name: '', + icon: Assets.presenceSensor, + ), + ); + + return product.icon ?? Assets.presenceSensor; + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart index 99ce3b1d..b6e77b10 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart @@ -8,6 +8,8 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_m import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -116,7 +118,7 @@ class _SidebarWidgetState extends State { children: [ Text('Communities', style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Colors.black, + color: ColorsManager.blackColor, )), GestureDetector( onTap: () => _navigateToBlank(context), @@ -184,6 +186,8 @@ class _SidebarWidgetState extends State { _selectedSpaceUuid = null; // Update the selected community }); + context.read().add(CommunitySelectedEvent()); + context.read().add( SelectCommunityEvent(selectedCommunity: community), ); diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_container_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_container_widget.dart index 78af5f10..6f52eb50 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_container_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_container_widget.dart @@ -78,7 +78,7 @@ class SpaceContainerWidget extends StatelessWidget { borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.5), + color: ColorsManager.lightGrayColor.withOpacity(0.5), spreadRadius: 2, blurRadius: 5, offset: const Offset(0, 3), // Shadow position diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart index e4ce27cc..6e1f50c1 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class SpaceWidget extends StatelessWidget { final String name; @@ -23,11 +24,11 @@ class SpaceWidget extends StatelessWidget { Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: Colors.white, + color: ColorsManager.whiteColors, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.5), + color: ColorsManager.lightGrayColor.withOpacity(0.5), spreadRadius: 5, blurRadius: 7, offset: const Offset(0, 3), @@ -36,7 +37,7 @@ class SpaceWidget extends StatelessWidget { ), child: Row( children: [ - const Icon(Icons.location_on, color: Colors.blue), + const Icon(Icons.location_on, color: ColorsManager.spaceColor), const SizedBox(width: 8), Text(name, style: const TextStyle(fontSize: 16)), ], diff --git a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart new file mode 100644 index 00000000..4a85348f --- /dev/null +++ b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart @@ -0,0 +1,148 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart'; + +class AssignTagBloc extends Bloc { + AssignTagBloc() : super(AssignTagInitial()) { + on((event, emit) { + final initialTags = event.initialTags ?? []; + + final existingTagCounts = {}; + for (var tag in initialTags) { + if (tag.product != null) { + existingTagCounts[tag.product!.uuid] = + (existingTagCounts[tag.product!.uuid] ?? 0) + 1; + } + } + + final allTags = []; + + for (var selectedProduct in event.addedProducts) { + final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; + + if (selectedProduct.count == 0 || + selectedProduct.count <= existingCount) { + allTags.addAll(initialTags + .where((tag) => tag.product?.uuid == selectedProduct.productId)); + continue; + } + + final missingCount = selectedProduct.count - existingCount; + + allTags.addAll(initialTags + .where((tag) => tag.product?.uuid == selectedProduct.productId)); + + if (missingCount > 0) { + allTags.addAll(List.generate( + missingCount, + (index) => Tag( + tag: '', + product: selectedProduct.product, + location: 'Main Space', + ), + )); + } + } + + emit(AssignTagLoaded( + tags: allTags, + isSaveEnabled: _validateTags(allTags), + )); + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + tags[event.index].tag = event.tag; + emit(AssignTagLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + errorMessage: _getValidationError(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + + // Use copyWith for immutability + tags[event.index] = + tags[event.index].copyWith(location: event.location); + + emit(AssignTagLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + + emit(AssignTagLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + errorMessage: _getValidationError(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { + final updatedTags = List.from(currentState.tags) + ..remove(event.tagToDelete); + + emit(AssignTagLoaded( + tags: updatedTags, + isSaveEnabled: _validateTags(updatedTags), + )); + } else { + emit(const AssignTagLoaded( + tags: [], + isSaveEnabled: false, + )); + } + }); + } + + bool _validateTags(List tags) { + if (tags.isEmpty) { + return false; + } + final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet(); + final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); + return uniqueTags.length == tags.length && !hasEmptyTag; + } + + String? _getValidationError(List tags) { + final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); + if (hasEmptyTag) return 'Tags cannot be empty.'; + final duplicateTags = tags + .map((tag) => tag.tag?.trim() ?? '') + .fold>({}, (map, tag) { + map[tag] = (map[tag] ?? 0) + 1; + return map; + }) + .entries + .where((entry) => entry.value > 1) + .map((entry) => entry.key) + .toList(); + + if (duplicateTags.isNotEmpty) { + return 'Duplicate tags found: ${duplicateTags.join(', ')}'; + } + + return null; + } +} diff --git a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart new file mode 100644 index 00000000..9116b094 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; + +abstract class AssignTagEvent extends Equatable { + const AssignTagEvent(); + + @override + List get props => []; +} + +class InitializeTags extends AssignTagEvent { + final List? initialTags; + final List addedProducts; + + const InitializeTags({ + required this.initialTags, + required this.addedProducts, + }); + + @override + List get props => [initialTags ?? [], addedProducts]; +} + +class UpdateTagEvent extends AssignTagEvent { + final int index; + final String tag; + + const UpdateTagEvent({required this.index, required this.tag}); + + @override + List get props => [index, tag]; +} + +class UpdateLocation extends AssignTagEvent { + final int index; + final String location; + + const UpdateLocation({required this.index, required this.location}); + + @override + List get props => [index, location]; +} + +class ValidateTags extends AssignTagEvent {} + +class DeleteTag extends AssignTagEvent { + final Tag tagToDelete; + final List tags; + + const DeleteTag({required this.tagToDelete, required this.tags}); + + @override + List get props => [tagToDelete, tags]; +} diff --git a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart new file mode 100644 index 00000000..19cf4435 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +abstract class AssignTagState extends Equatable { + const AssignTagState(); + + @override + List get props => []; +} + +class AssignTagInitial extends AssignTagState {} + +class AssignTagLoading extends AssignTagState {} + +class AssignTagLoaded extends AssignTagState { + final List tags; + final bool isSaveEnabled; + final String? errorMessage; + + const AssignTagLoaded({ + required this.tags, + required this.isSaveEnabled, + this.errorMessage, + }); + + @override + List get props => [tags, isSaveEnabled]; +} + +class AssignTagError extends AssignTagState { + final String errorMessage; + + const AssignTagError(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart b/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart new file mode 100644 index 00000000..959c83df --- /dev/null +++ b/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart @@ -0,0 +1,340 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AssignTagDialog extends StatelessWidget { + final List? products; + final List? subspaces; + final List? initialTags; + final ValueChanged>? onTagsAssigned; + final List addedProducts; + final List? allTags; + final String spaceName; + final String title; + final Function(List, List?)? onSave; + + const AssignTagDialog( + {Key? key, + required this.products, + required this.subspaces, + required this.addedProducts, + this.initialTags, + this.onTagsAssigned, + this.allTags, + required this.spaceName, + required this.title, + this.onSave}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final List locations = + (subspaces ?? []).map((subspace) => subspace.subspaceName).toList(); + return BlocProvider( + create: (_) => AssignTagBloc() + ..add(InitializeTags( + initialTags: initialTags, + addedProducts: addedProducts, + )), + child: BlocBuilder( + builder: (context, state) { + if (state is AssignTagLoaded) { + final controllers = List.generate( + state.tags.length, + (index) => TextEditingController(text: state.tags[index].tag), + ); + + return AlertDialog( + title: Text(title), + backgroundColor: ColorsManager.whiteColors, + content: SingleChildScrollView( + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + ColorsManager.dataHeaderGrey), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + DataColumn( + label: Text('#', + style: + Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Device', + style: + Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Tag', + style: + Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Location', + style: + Theme.of(context).textTheme.bodyMedium)), + ], + rows: state.tags.isEmpty + ? [ + const DataRow(cells: [ + DataCell( + Center( + child: Text( + 'No Data Available', + style: TextStyle( + fontSize: 14, + color: ColorsManager.lightGrayColor, + ), + ), + ), + ), + DataCell(SizedBox()), + DataCell(SizedBox()), + DataCell(SizedBox()), + ]) + ] + : List.generate(state.tags.length, (index) { + final tag = state.tags[index]; + final controller = controllers[index]; + + return DataRow( + cells: [ + DataCell(Text(index.toString())), + DataCell( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + tag.product?.name ?? 'Unknown', + overflow: TextOverflow.ellipsis, + )), + IconButton( + icon: const Icon(Icons.close, + color: ColorsManager.warningRed, + size: 16), + onPressed: () { + context.read().add( + DeleteTag( + tagToDelete: tag, + tags: state.tags)); + }, + tooltip: 'Delete Tag', + ) + ], + ), + ), + DataCell( + Row( + children: [ + Expanded( + child: TextFormField( + controller: controller, + onChanged: (value) { + context + .read() + .add(UpdateTagEvent( + index: index, + tag: value.trim(), + )); + }, + decoration: const InputDecoration( + hintText: 'Enter Tag', + border: InputBorder.none, + ), + style: const TextStyle( + fontSize: 14, + color: ColorsManager.blackColor, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + 0.15, + child: PopupMenuButton( + color: ColorsManager.whiteColors, + icon: const Icon( + Icons.arrow_drop_down, + color: + ColorsManager.blackColor), + onSelected: (value) { + controller.text = value; + context + .read() + .add(UpdateTagEvent( + index: index, + tag: value, + )); + }, + itemBuilder: (context) { + return (allTags ?? []) + .where((tagValue) => !state + .tags + .map((e) => e.tag) + .contains(tagValue)) + .map((tagValue) { + return PopupMenuItem( + textStyle: const TextStyle( + color: ColorsManager + .textPrimaryColor), + value: tagValue, + child: ConstrainedBox( + constraints: + BoxConstraints( + minWidth: MediaQuery.of( + context) + .size + .width * + 0.15, + maxWidth: MediaQuery.of( + context) + .size + .width * + 0.15, + ), + child: Text( + tagValue, + overflow: TextOverflow + .ellipsis, + ), + )); + }).toList(); + }, + ), + ), + ], + ), + ), + DataCell( + DropdownButtonHideUnderline( + child: DropdownButton( + value: tag.location ?? 'Main', + dropdownColor: ColorsManager + .whiteColors, // Dropdown background + style: const TextStyle( + color: Colors + .black), // Style for selected text + items: [ + const DropdownMenuItem( + value: 'Main Space', + child: Text( + 'Main Space', + style: TextStyle( + color: ColorsManager + .textPrimaryColor), + ), + ), + ...locations.map((location) { + return DropdownMenuItem( + value: location, + child: Text( + location, + style: const TextStyle( + color: ColorsManager + .textPrimaryColor), + ), + ); + }).toList(), + ], + onChanged: (value) { + if (value != null) { + context + .read() + .add(UpdateLocation( + index: index, + location: value, + )); + } + }, + ), + ), + ), + ], + ); + }), + ), + ), + if (state.errorMessage != null) + Text( + state.errorMessage!, + style: const TextStyle(color: ColorsManager.warningRed), + ), + ], + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 10), + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + borderRadius: 10, + backgroundColor: state.isSaveEnabled + ? ColorsManager.secondaryColor + : ColorsManager.grayColor, + foregroundColor: ColorsManager.whiteColors, + onPressed: state.isSaveEnabled + ? () async { + Navigator.of(context).pop(); + final assignedTags = {}; + for (var tag in state.tags) { + if (tag.location == null || + subspaces == null) { + continue; + } + for (var subspace in subspaces!) { + if (tag.location == subspace.subspaceName) { + subspace.tags ??= []; + subspace.tags!.add(tag); + assignedTags.add(tag); + break; + } + } + } + onSave!(state.tags,subspaces); + } + : null, + child: const Text('Save'), + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ); + } else if (state is AssignTagLoading) { + return const Center(child: CircularProgressIndicator()); + } else { + return const Center(child: Text('Something went wrong.')); + } + }, + ), + ); + } +} diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart new file mode 100644 index 00000000..92d7c0e1 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart @@ -0,0 +1,155 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +class AssignTagModelBloc + extends Bloc { + AssignTagModelBloc() : super(AssignTagModelInitial()) { + on((event, emit) { + final initialTags = event.initialTags ?? []; + + final existingTagCounts = {}; + for (var tag in initialTags) { + if (tag.product != null) { + existingTagCounts[tag.product!.uuid] = + (existingTagCounts[tag.product!.uuid] ?? 0) + 1; + } + } + + final allTags = []; + + for (var selectedProduct in event.addedProducts) { + final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; + + if (selectedProduct.count == 0 || + selectedProduct.count <= existingCount) { + allTags.addAll(initialTags + .where((tag) => tag.product?.uuid == selectedProduct.productId)); + continue; + } + + final missingCount = selectedProduct.count - existingCount; + + allTags.addAll(initialTags + .where((tag) => tag.product?.uuid == selectedProduct.productId)); + + if (missingCount > 0) { + allTags.addAll(List.generate( + missingCount, + (index) => TagModel( + tag: '', + product: selectedProduct.product, + location: 'Main Space', + ), + )); + } + } + + emit(AssignTagModelLoaded( + tags: allTags, + isSaveEnabled: _validateTags(allTags), + errorMessage: '')); + }); + + on((event, emit) { + final currentState = state; + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + tags[event.index].tag = event.tag; + emit(AssignTagModelLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + errorMessage: _getValidationError(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + + // Use copyWith for immutability + tags[event.index] = + tags[event.index].copyWith(location: event.location); + + emit(AssignTagModelLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + + emit(AssignTagModelLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + errorMessage: _getValidationError(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final updatedTags = List.from(currentState.tags) + ..remove(event.tagToDelete); + + emit(AssignTagModelLoaded( + tags: updatedTags, + isSaveEnabled: _validateTags(updatedTags), + )); + } else { + emit(const AssignTagModelLoaded( + tags: [], + isSaveEnabled: false, + )); + } + }); + } + + bool _validateTags(List tags) { + + final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet(); + final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); + final isValid = uniqueTags.length == tags.length && !hasEmptyTag; + return isValid; + } + + String? _getValidationError(List tags) { + final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); + if (hasEmptyTag) { + return 'Tags cannot be empty.'; + } + + // Check for duplicate tags + final duplicateTags = tags + .map((tag) => tag.tag?.trim() ?? '') + .fold>({}, (map, tag) { + map[tag] = (map[tag] ?? 0) + 1; + return map; + }) + .entries + .where((entry) => entry.value > 1) + .map((entry) => entry.key) + .toList(); + + if (duplicateTags.isNotEmpty) { + return 'Duplicate tags found: ${duplicateTags.join(', ')}'; + } + + return null; + } +} diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart new file mode 100644 index 00000000..38642d96 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; + +abstract class AssignTagModelEvent extends Equatable { + const AssignTagModelEvent(); + + @override + List get props => []; +} + +class InitializeTagModels extends AssignTagModelEvent { + final List initialTags; + final List addedProducts; + + const InitializeTagModels({ + this.initialTags = const [], + required this.addedProducts, + }); + + @override + List get props => [initialTags, addedProducts]; +} + +class UpdateTag extends AssignTagModelEvent { + final int index; + final String tag; + + const UpdateTag({required this.index, required this.tag}); + + @override + List get props => [index, tag]; +} + +class UpdateLocation extends AssignTagModelEvent { + final int index; + final String location; + + const UpdateLocation({required this.index, required this.location}); + + @override + List get props => [index, location]; +} + +class ValidateTagModels extends AssignTagModelEvent {} + +class DeleteTagModel extends AssignTagModelEvent { + final TagModel tagToDelete; + final List tags; + + const DeleteTagModel({required this.tagToDelete, required this.tags}); + + @override + List get props => [tagToDelete, tags]; +} diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart new file mode 100644 index 00000000..a51a9e8f --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +abstract class AssignTagModelState extends Equatable { + const AssignTagModelState(); + + @override + List get props => []; +} + +class AssignTagModelInitial extends AssignTagModelState {} + +class AssignTagModelLoading extends AssignTagModelState {} + +class AssignTagModelLoaded extends AssignTagModelState { + final List tags; + final bool isSaveEnabled; + final String? errorMessage; + + const AssignTagModelLoaded({ + required this.tags, + required this.isSaveEnabled, + this.errorMessage, + }); + + @override + List get props => [tags, isSaveEnabled, errorMessage]; +} + +class AssignTagModelError extends AssignTagModelState { + final String errorMessage; + + const AssignTagModelError(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart new file mode 100644 index 00000000..fc778436 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart @@ -0,0 +1,433 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/dialog_dropdown.dart'; +import 'package:syncrow_web/common/dialog_textfield_dropdown.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AssignTagModelsDialog extends StatelessWidget { + final List? products; + final List? subspaces; + final SpaceTemplateModel? spaceModel; + + final List initialTags; + final ValueChanged>? onTagsAssigned; + final List addedProducts; + final List? allTags; + final String spaceName; + final String title; + final BuildContext? pageContext; + final List? otherSpaceModels; + + const AssignTagModelsDialog( + {Key? key, + required this.products, + required this.subspaces, + required this.addedProducts, + required this.initialTags, + this.onTagsAssigned, + this.allTags, + required this.spaceName, + required this.title, + this.pageContext, + this.otherSpaceModels, + this.spaceModel}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final List locations = (subspaces ?? []) + .map((subspace) => subspace.subspaceName) + .toList() + ..add('Main Space'); + + return BlocProvider( + create: (_) => AssignTagModelBloc() + ..add(InitializeTagModels( + initialTags: initialTags, + addedProducts: addedProducts, + )), + child: BlocListener( + listener: (context, state) {}, + child: BlocBuilder( + builder: (context, state) { + if (state is AssignTagModelLoaded) { + final controllers = List.generate( + state.tags.length, + (index) => TextEditingController(text: state.tags[index].tag), + ); + + return AlertDialog( + title: Text(title), + backgroundColor: ColorsManager.whiteColors, + content: SingleChildScrollView( + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + ColorsManager.dataHeaderGrey), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + DataColumn( + label: Text('#', + style: Theme.of(context) + .textTheme + .bodyMedium)), + DataColumn( + label: Text('Device', + style: Theme.of(context) + .textTheme + .bodyMedium)), + DataColumn( + numeric: false, + headingRowAlignment: MainAxisAlignment.start, + label: Text('Tag', + style: Theme.of(context) + .textTheme + .bodyMedium)), + DataColumn( + label: Text('Location', + style: Theme.of(context) + .textTheme + .bodyMedium)), + ], + rows: state.tags.isEmpty + ? [ + const DataRow(cells: [ + DataCell( + Center( + child: Text( + 'No Data Available', + style: TextStyle( + fontSize: 14, + color: + ColorsManager.lightGrayColor, + ), + ), + ), + ), + DataCell(SizedBox()), + DataCell(SizedBox()), + DataCell(SizedBox()), + ]) + ] + : List.generate(state.tags.length, (index) { + final tag = state.tags[index]; + final controller = controllers[index]; + final availableTags = getAvailableTags( + allTags ?? [], state.tags, tag); + + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + tag.product?.name ?? 'Unknown', + overflow: TextOverflow.ellipsis, + )), + const SizedBox(width: 10), + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager + .lightGrayColor, + width: 1.0, + ), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: ColorsManager + .lightGreyColor, + size: 16, + ), + onPressed: () { + context + .read< + AssignTagModelBloc>() + .add(DeleteTagModel( + tagToDelete: tag, + tags: state.tags)); + }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: + const BoxConstraints(), + ), + ), + ], + ), + ), + DataCell( + Container( + alignment: Alignment + .centerLeft, // Align cell content to the left + child: SizedBox( + width: double + .infinity, // Ensure full width for dropdown + child: DialogTextfieldDropdown( + items: availableTags, + initialValue: tag.tag, + onSelected: (value) { + controller.text = value; + context + .read< + AssignTagModelBloc>() + .add(UpdateTag( + index: index, + tag: value, + )); + }, + ), + ), + ), + ), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: locations, + selectedValue: + tag.location ?? 'Main Space', + onSelected: (value) { + context + .read< + AssignTagModelBloc>() + .add(UpdateLocation( + index: index, + location: value, + )); + }, + )), + ), + ], + ); + }), + ), + ), + if (state.errorMessage != null) + Text( + state.errorMessage!, + style: const TextStyle( + color: ColorsManager.warningRed), + ), + ], + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 10), + Expanded( + child: Builder( + builder: (buttonContext) => CancelButton( + label: 'Add New Device', + onPressed: () async { + for (var tag in state.tags) { + if (tag.location == null || + subspaces == null) { + continue; + } + + final previousTagSubspace = + checkTagExistInSubspace( + tag, subspaces ?? []); + + if (tag.location == 'Main Space') { + removeTagFromSubspace( + tag, previousTagSubspace); + } else if (tag.location != + previousTagSubspace?.subspaceName) { + removeTagFromSubspace( + tag, previousTagSubspace); + moveToNewSubspace(tag, subspaces ?? []); + state.tags.removeWhere( + (t) => t.internalId == tag.internalId); + } else { + updateTagInSubspace( + tag, previousTagSubspace); + state.tags.removeWhere( + (t) => t.internalId == tag.internalId); + } + } + if (context.mounted) { + Navigator.of(context).pop(); + + await showDialog( + barrierDismissible: false, + context: context, + builder: (dialogContext) => + AddDeviceTypeModelWidget( + products: products, + subspaces: subspaces, + isCreate: false, + initialSelectedProducts: + addedProducts, + allTags: allTags, + spaceName: spaceName, + otherSpaceModels: otherSpaceModels, + spaceTagModels: state.tags, + pageContext: pageContext, + spaceModel: SpaceTemplateModel( + modelName: spaceName, + tags: state.tags, + uuid: spaceModel?.uuid, + internalId: + spaceModel?.internalId, + subspaceModels: subspaces)), + ); + } + }, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + borderRadius: 10, + backgroundColor: state.isSaveEnabled + ? ColorsManager.secondaryColor + : ColorsManager.grayColor, + foregroundColor: ColorsManager.whiteColors, + onPressed: state.isSaveEnabled + ? () async { + for (var tag in state.tags) { + if (tag.location == null || + subspaces == null) { + continue; + } + + final previousTagSubspace = + checkTagExistInSubspace( + tag, subspaces ?? []); + + if (tag.location == 'Main Space') { + removeTagFromSubspace( + tag, previousTagSubspace); + } else if (tag.location != + previousTagSubspace?.subspaceName) { + removeTagFromSubspace( + tag, previousTagSubspace); + moveToNewSubspace(tag, subspaces ?? []); + state.tags.removeWhere((t) => + t.internalId == tag.internalId); + } else { + updateTagInSubspace( + tag, previousTagSubspace); + state.tags.removeWhere((t) => + t.internalId == tag.internalId); + } + } + Navigator.of(context) + .popUntil((route) => route.isFirst); + + await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return CreateSpaceModelDialog( + products: products, + allTags: allTags, + pageContext: pageContext, + otherSpaceModels: otherSpaceModels, + spaceModel: SpaceTemplateModel( + modelName: spaceName, + tags: state.tags, + uuid: spaceModel?.uuid, + internalId: + spaceModel?.internalId, + subspaceModels: subspaces), + ); + }, + ); + } + : null, + child: const Text('Save'), + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ); + } else if (state is AssignTagModelLoading) { + return const Center(child: CircularProgressIndicator()); + } else { + return const Center(child: Text('Something went wrong.')); + } + }, + ), + )); + } + + List getAvailableTags( + List allTags, List currentTags, TagModel currentTag) { + return allTags + .where((tagValue) => !currentTags + .where((e) => e != currentTag) // Exclude the current row + .map((e) => e.tag) + .contains(tagValue)) + .toList(); + } + + void removeTagFromSubspace(TagModel tag, SubspaceTemplateModel? subspace) { + subspace?.tags?.removeWhere((t) => t.internalId == tag.internalId); + } + + SubspaceTemplateModel? checkTagExistInSubspace( + TagModel tag, List? subspaces) { + if (subspaces == null) return null; + for (var subspace in subspaces) { + if (subspace.tags == null) return null; + for (var t in subspace.tags!) { + if (tag.internalId == t.internalId) return subspace; + } + } + return null; + } + + void moveToNewSubspace(TagModel tag, List subspaces) { + final targetSubspace = subspaces + .firstWhere((subspace) => subspace.subspaceName == tag.location); + + targetSubspace.tags ??= []; + if (targetSubspace.tags?.any((t) => t.internalId == tag.internalId) != + true) { + targetSubspace.tags?.add(tag); + } + } + + void updateTagInSubspace(TagModel tag, SubspaceTemplateModel? subspace) { + final currentTag = subspace?.tags?.firstWhere( + (t) => t.internalId == tag.internalId, + ); + if (currentTag != null) { + currentTag.tag = tag.tag; + } + } +} diff --git a/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart new file mode 100644 index 00000000..6a072e4a --- /dev/null +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart @@ -0,0 +1,57 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/utils/constants/action_enum.dart'; + +import 'subspace_event.dart'; +import 'subspace_state.dart'; + +class SubSpaceBloc extends Bloc { + SubSpaceBloc() : super(SubSpaceState([], [], '')) { + on((event, emit) { + final existingNames = + state.subSpaces.map((e) => e.subspaceName).toSet(); + + if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) { + emit(SubSpaceState( + state.subSpaces, + state.updatedSubSpaceModels, + 'Subspace name already exists.', + )); + } else { + final updatedSubSpaces = List.from(state.subSpaces) + ..add(event.subSpace); + + emit(SubSpaceState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '', + )); + } + }); + + // Handle RemoveSubSpace Event + on((event, emit) { + final updatedSubSpaces = List.from(state.subSpaces) + ..remove(event.subSpace); + + final updatedSubspaceModels = List.from( + state.updatedSubSpaceModels, + ); + + if (event.subSpace.uuid?.isNotEmpty ?? false) { + updatedSubspaceModels.add(UpdateSubspaceModel( + action: Action.delete, + uuid: event.subSpace.uuid!, + )); + } + + emit(SubSpaceState( + updatedSubSpaces, + updatedSubspaceModels, + '', // Clear error message + )); + }); + + // Handle UpdateSubSpace Event + } +} diff --git a/lib/pages/spaces_management/create_subspace/bloc/subspace_event.dart b/lib/pages/spaces_management/create_subspace/bloc/subspace_event.dart new file mode 100644 index 00000000..cdb62ea1 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_event.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; + +abstract class SubSpaceEvent {} + +class AddSubSpace extends SubSpaceEvent { + final SubspaceModel subSpace; + AddSubSpace(this.subSpace); +} + +class RemoveSubSpace extends SubSpaceEvent { + final SubspaceModel subSpace; + RemoveSubSpace(this.subSpace); +} + +class UpdateSubSpace extends SubSpaceEvent { + final SubspaceModel updatedSubSpace; + UpdateSubSpace(this.updatedSubSpace); +} diff --git a/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart b/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart new file mode 100644 index 00000000..d1374ea1 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart @@ -0,0 +1,26 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; + +class SubSpaceState { + final List subSpaces; + final List updatedSubSpaceModels; + final String errorMessage; + + SubSpaceState( + this.subSpaces, + this.updatedSubSpaceModels, + this.errorMessage, + ); + + + SubSpaceState copyWith({ + List? subSpaces, + List? updatedSubSpaceModels, + String? errorMessage, + }) { + return SubSpaceState( + subSpaces ?? this.subSpaces, + updatedSubSpaceModels ?? this.updatedSubSpaceModels, + errorMessage ?? this.errorMessage, + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart b/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart new file mode 100644 index 00000000..6fd0b936 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSubSpaceDialog extends StatelessWidget { + final bool isEdit; + final String dialogTitle; + final List? existingSubSpaces; + final String? spaceName; + final List? spaceTags; + final List? products; + final Function(List?)? onSave; + + const CreateSubSpaceDialog( + {Key? key, + required this.isEdit, + required this.dialogTitle, + this.existingSubSpaces, + required this.spaceName, + required this.spaceTags, + required this.products, + required this.onSave}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final textController = TextEditingController(); + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: BlocProvider( + create: (_) { + final bloc = SubSpaceBloc(); + if (existingSubSpaces != null) { + for (var subSpace in existingSubSpaces!) { + bloc.add(AddSubSpace(subSpace)); + } + } + return bloc; + }, + child: BlocBuilder( + builder: (context, state) { + return Container( + color: ColorsManager.whiteColors, + child: SizedBox( + width: screenWidth * 0.35, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + dialogTitle, + style: Theme.of(context) + .textTheme + .headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + const SizedBox(height: 16), + Container( + width: screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 16.0), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...state.subSpaces.map( + (subSpace) => Chip( + label: Text( + subSpace.subspaceName, + style: const TextStyle( + color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide( + color: ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const Icon( + Icons.close, + size: 16, + color: ColorsManager.lightGrayColor, + ), + ), + onDeleted: () => context + .read() + .add(RemoveSubSpace(subSpace)), + ), + ), + SizedBox( + width: 200, + child: TextField( + controller: textController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: state.subSpaces.isEmpty + ? 'Please enter the name' + : null, + hintStyle: const TextStyle( + color: ColorsManager.lightGrayColor), + ), + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + context.read().add( + AddSubSpace(SubspaceModel( + subspaceName: value.trim(), + disabled: false))); + textController.clear(); + } + }, + style: const TextStyle( + color: ColorsManager.blackColor), + ), + ), + if (state.errorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + state.errorMessage, + style: const TextStyle( + color: ColorsManager.warningRed, + fontSize: 12, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () async {}, + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: () async { + final subSpaces = context + .read() + .state + .subSpaces; + onSave!(subSpaces); + Navigator.of(context).pop(); + }, + backgroundColor: ColorsManager.secondaryColor, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ), + ), + )); + }, + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart new file mode 100644 index 00000000..1e8d0ddc --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart @@ -0,0 +1,105 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/utils/constants/action_enum.dart'; + +class SubSpaceModelBloc extends Bloc { + SubSpaceModelBloc() : super(SubSpaceModelState([], [], '', {})) { + // Handle AddSubSpaceModel Event + on((event, emit) { + final existingNames = + state.subSpaces.map((e) => e.subspaceName.toLowerCase()).toSet(); + + if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) { + final updatedDuplicates = Set.from(state.duplicates) + ..add(event.subSpace.subspaceName.toLowerCase()); + final updatedSubSpaces = + List.from(state.subSpaces) + ..add(event.subSpace); + emit(SubSpaceModelState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '*Duplicated sub-space name', + updatedDuplicates, + )); + } else { + // Add subspace if no duplicate exists + final updatedSubSpaces = + List.from(state.subSpaces) + ..add(event.subSpace); + + emit(SubSpaceModelState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '', + state.duplicates, +// Clear error message + )); + } + }); + + // Handle RemoveSubSpaceModel Event + on((event, emit) { + final updatedSubSpaces = List.from(state.subSpaces) + ..remove(event.subSpace); + + final updatedSubspaceModels = List.from( + state.updatedSubSpaceModels, + ); + final nameOccurrences = {}; + for (final subSpace in updatedSubSpaces) { + final lowerName = subSpace.subspaceName.toLowerCase(); + nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1; + } + + final updatedDuplicates = nameOccurrences.entries + .where((entry) => entry.value > 1) + .map((entry) => entry.key) + .toSet(); + + if (event.subSpace.uuid?.isNotEmpty ?? false) { + updatedSubspaceModels.add(UpdateSubspaceTemplateModel( + action: Action.delete, + uuid: event.subSpace.uuid!, + )); + } + + emit(SubSpaceModelState( + updatedSubSpaces, + updatedSubspaceModels, + '', + updatedDuplicates, +// Clear error message + )); + }); + + // Handle UpdateSubSpaceModel Event + on((event, emit) { + final updatedSubSpaces = state.subSpaces.map((subSpace) { + if (subSpace.uuid == event.updatedSubSpace.uuid) { + return event.updatedSubSpace; + } + return subSpace; + }).toList(); + + final updatedSubspaceModels = List.from( + state.updatedSubSpaceModels, + ); + + updatedSubspaceModels.add(UpdateSubspaceTemplateModel( + action: Action.update, + uuid: event.updatedSubSpace.uuid!, + )); + + emit(SubSpaceModelState( + updatedSubSpaces, + updatedSubspaceModels, + '', + state.duplicates, +// Clear error message + )); + }); + } +} diff --git a/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart new file mode 100644 index 00000000..63629472 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; + +abstract class SubSpaceModelEvent {} + +class AddSubSpaceModel extends SubSpaceModelEvent { + final SubspaceTemplateModel subSpace; + AddSubSpaceModel(this.subSpace); +} + +class RemoveSubSpaceModel extends SubSpaceModelEvent { + final SubspaceTemplateModel subSpace; + RemoveSubSpaceModel(this.subSpace); +} + +class UpdateSubSpaceModel extends SubSpaceModelEvent { + final SubspaceTemplateModel updatedSubSpace; + UpdateSubSpaceModel(this.updatedSubSpace); +} diff --git a/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart new file mode 100644 index 00000000..207d9601 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart @@ -0,0 +1,30 @@ +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; + +class SubSpaceModelState { + final List subSpaces; + final List updatedSubSpaceModels; + final String errorMessage; + final Set duplicates; + + SubSpaceModelState( + this.subSpaces, + this.updatedSubSpaceModels, + this.errorMessage, + this.duplicates, + ); + + SubSpaceModelState copyWith({ + List? subSpaces, + List? updatedSubSpaceModels, + String? errorMessage, + Set? duplicates, + }) { + return SubSpaceModelState( + subSpaces ?? this.subSpaces, + updatedSubSpaceModels ?? this.updatedSubSpaceModels, + errorMessage ?? this.errorMessage, + duplicates ?? this.duplicates, + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart b/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart new file mode 100644 index 00000000..4c0cb99f --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSubSpaceModelDialog extends StatelessWidget { + final bool isEdit; + final String dialogTitle; + final List? existingSubSpaces; + final void Function(List newSubspaces)? onUpdate; + + const CreateSubSpaceModelDialog( + {Key? key, + required this.isEdit, + required this.dialogTitle, + this.existingSubSpaces, + this.onUpdate}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final textController = TextEditingController(); + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: BlocProvider( + create: (_) { + final bloc = SubSpaceModelBloc(); + if (existingSubSpaces != null) { + for (var subSpace in existingSubSpaces!) { + bloc.add(AddSubSpaceModel(subSpace)); + } + } + return bloc; + }, + child: BlocBuilder( + builder: (context, state) { + return Container( + color: ColorsManager.whiteColors, + child: SizedBox( + width: screenWidth * 0.3, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + dialogTitle, + style: Theme.of(context) + .textTheme + .headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + const SizedBox(height: 16), + Container( + width: screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 16.0), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...state.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = + subSpace.subspaceName.toLowerCase(); + + final duplicateIndices = state.subSpaces + .asMap() + .entries + .where((e) => + e.value.subspaceName.toLowerCase() == + lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = + duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + + return Chip( + label: Text( + subSpace.subspaceName, + style: const TextStyle( + color: ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate + ? ColorsManager.red + : ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const Icon( + Icons.close, + size: 16, + color: ColorsManager.lightGrayColor, + ), + ), + onDeleted: () => context + .read() + .add(RemoveSubSpaceModel(subSpace)), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: textController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: state.subSpaces.isEmpty + ? 'Please enter the name' + : null, + hintStyle: const TextStyle( + color: ColorsManager.lightGrayColor), + ), + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + context.read().add( + AddSubSpaceModel( + SubspaceTemplateModel( + subspaceName: value.trim(), + disabled: false))); + textController.clear(); + } + }, + style: const TextStyle( + color: ColorsManager.blackColor), + ), + ), + ], + ), + ), + if (state.errorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text( + state.errorMessage, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 12, + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: (state.errorMessage.isNotEmpty) + ? null + : () async { + final subSpaces = context + .read() + .state + .subSpaces; + Navigator.of(context).pop(); + if (onUpdate != null) { + onUpdate!(subSpaces); + } + }, + backgroundColor: ColorsManager.secondaryColor, + borderRadius: 10, + foregroundColor: state.errorMessage.isNotEmpty + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ), + ), + )); + }, + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/helper/tag_helper.dart b/lib/pages/spaces_management/helper/tag_helper.dart new file mode 100644 index 00000000..d4a0ea55 --- /dev/null +++ b/lib/pages/spaces_management/helper/tag_helper.dart @@ -0,0 +1,81 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +class TagHelper { + static List generateInitialTags({ + List? spaceTagModels, + List? subspaces, + }) { + final List initialTags = []; + + if (spaceTagModels != null) { + initialTags.addAll(spaceTagModels); + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + for (var existingTag in subspace.tags!) { + initialTags.addAll( + subspace.tags!.map( + (tag) => tag.copyWith( + location: subspace.subspaceName, + internalId: existingTag.internalId, + tag: existingTag.tag), + ), + ); + } + } + } + } + return initialTags; + } + + static Map groupTags(List tags) { + final Map groupedTags = {}; + for (var tag in tags) { + if (tag.product != null) { + final product = tag.product!; + groupedTags[product] = (groupedTags[product] ?? 0) + 1; + } + } + return groupedTags; + } + + static List createInitialSelectedProducts( + List? tags, List? subspaces) { + final Map productCounts = {}; + + if (tags != null) { + for (var tag in tags) { + if (tag.product != null) { + productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1; + } + } + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + for (var tag in subspace.tags!) { + if (tag.product != null) { + productCounts[tag.product!] = + (productCounts[tag.product!] ?? 0) + 1; + } + } + } + } + } + + return productCounts.entries + .map((entry) => SelectedProduct( + productId: entry.key.uuid, + count: entry.value, + productName: entry.key.name ?? 'Unnamed', + product: entry.key, + )) + .toList(); + } +} diff --git a/lib/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart b/lib/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart new file mode 100644 index 00000000..aa9a446d --- /dev/null +++ b/lib/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart @@ -0,0 +1,12 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart'; + + +class SpaceModelBloc extends Bloc { + SpaceModelBloc() : super(SpaceModelInitial()) { + on((event, emit) { + emit(SpaceModelSelectedState(event.selectedIndex)); + }); + } +} \ No newline at end of file diff --git a/lib/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart b/lib/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart new file mode 100644 index 00000000..8bff0202 --- /dev/null +++ b/lib/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart @@ -0,0 +1,7 @@ +abstract class SpaceModelEvent {} + +class SpaceModelSelectedEvent extends SpaceModelEvent { + final int selectedIndex; + + SpaceModelSelectedEvent(this.selectedIndex); +} diff --git a/lib/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart b/lib/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart new file mode 100644 index 00000000..cc745e4d --- /dev/null +++ b/lib/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart @@ -0,0 +1,9 @@ +abstract class SpaceModelState {} + +class SpaceModelInitial extends SpaceModelState {} + +class SpaceModelSelectedState extends SpaceModelState { + final int selectedIndex; + + SpaceModelSelectedState(this.selectedIndex); +} diff --git a/lib/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart b/lib/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart new file mode 100644 index 00000000..69023857 --- /dev/null +++ b/lib/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class LinkSpaceModelDialog extends StatelessWidget { + final Function(SpaceTemplateModel?)? onSave; + final int? initialSelectedIndex; + final List spaceModels; + + const LinkSpaceModelDialog({ + Key? key, + this.onSave, + this.initialSelectedIndex, + required this.spaceModels, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SpaceModelBloc() + ..add( + SpaceModelSelectedEvent(initialSelectedIndex ?? -1), + ), + child: Builder( + builder: (context) { + final bloc = context.read(); + return AlertDialog( + backgroundColor: ColorsManager.whiteColors, + title: const Text('Link a space model'), + content: spaceModels.isNotEmpty + ? Container( + color: ColorsManager.textFieldGreyColor, + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.height * 0.6, + child: BlocBuilder( + builder: (context, state) { + int selectedIndex = -1; + if (state is SpaceModelSelectedState) { + selectedIndex = state.selectedIndex; + } + return GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + childAspectRatio: 3, + ), + itemCount: spaceModels.length, + itemBuilder: (BuildContext context, int index) { + final model = spaceModels[index]; + final isSelected = selectedIndex == index; + return GestureDetector( + onTap: () { + bloc.add(SpaceModelSelectedEvent(index)); + }, + child: Container( + margin: const EdgeInsets.all(10.0), + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? ColorsManager.spaceColor + : Colors.transparent, + width: 2.0, + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: SpaceModelCardWidget(model: model,), + ), + ); + }, + ); + }, + ), + ) + : const Text('No space models available.'), + actions: [ + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CancelButton( + onPressed: () { + Navigator.of(context).pop(); + }, + label: 'Cancel', + ), + const SizedBox(width: 10), + BlocBuilder( + builder: (context, state) { + final isEnabled = state is SpaceModelSelectedState && + state.selectedIndex >= 0; + return SizedBox( + width: 140, + child: DefaultButton( + height: 40, + borderRadius: 10, + onPressed: isEnabled + ? () { + if (onSave != null) { + final selectedModel = + spaceModels[state.selectedIndex]; + + onSave!(selectedModel); + } + Navigator.of(context).pop(); + } + : null, + child: const Text('Save'), + ), + ); + }, + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart b/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart new file mode 100644 index 00000000..d8b39216 --- /dev/null +++ b/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart @@ -0,0 +1,353 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart'; +import 'package:syncrow_web/services/space_model_mang_api.dart'; +import 'package:syncrow_web/utils/constants/action_enum.dart'; + +class CreateSpaceModelBloc + extends Bloc { + SpaceTemplateModel? _space; + + final SpaceModelManagementApi _api; + + CreateSpaceModelBloc(this._api) : super(CreateSpaceModelInitial()) { + on((event, emit) async { + try { + late SpaceTemplateModel spaceTemplate = event.spaceTemplate; + + final tagBodyModels = + spaceTemplate.tags?.map((tag) => tag.toTagBodyModel()).toList() ?? + []; + + final subspaceTemplateBodyModels = + spaceTemplate.subspaceModels?.map((subspaceModel) { + final tagsubspaceBodyModels = subspaceModel.tags + ?.map((tag) => tag.toTagBodyModel()) + .toList() ?? + []; + return CreateSubspaceTemplateModel() + ..subspaceName = subspaceModel.subspaceName + ..tags = tagsubspaceBodyModels; + }).toList() ?? + []; + final spaceModelBody = CreateSpaceTemplateBodyModel( + modelName: spaceTemplate.modelName, + tags: tagBodyModels, + subspaceModels: subspaceTemplateBodyModels); + + final newSpaceTemplate = await _api.createSpaceModel(spaceModelBody); + spaceTemplate.uuid = newSpaceTemplate?.uuid ?? ''; + + if (newSpaceTemplate != null) { + emit(CreateSpaceModelLoaded(spaceTemplate)); + + if (event.onCreate != null) { + event.onCreate!(spaceTemplate); + } + } + } catch (e) { + emit(CreateSpaceModelError('Error creating space model')); + } + }); + + on((event, emit) { + emit(CreateSpaceModelLoading()); + Future.delayed(const Duration(seconds: 1), () { + if (_space != null) { + emit(CreateSpaceModelLoaded(_space!)); + } else { + emit(CreateSpaceModelError("No space template found")); + } + }); + }); + + on((event, emit) { + _space = event.spaceTemplate; + emit(CreateSpaceModelLoaded(_space!)); + }); + on((event, emit) { + final currentState = state; + + if (currentState is CreateSpaceModelLoaded) { + final eventSubspaceIds = + event.subspaces.map((e) => e.internalId).toSet(); + + // Update or retain subspaces + final updatedSubspaces = currentState.space.subspaceModels + ?.where((subspace) => + eventSubspaceIds.contains(subspace.internalId)) + .map((subspace) { + final matchingEventSubspace = event.subspaces.firstWhere( + (e) => e.internalId == subspace.internalId, + orElse: () => subspace, + ); + + // Update the subspace's tags + final eventTagIds = matchingEventSubspace.tags + ?.map((e) => e.internalId) + .toSet() ?? + {}; + + final updatedTags = [ + ...?subspace.tags?.map((tag) { + final matchingTag = + matchingEventSubspace.tags?.firstWhere( + (e) => e.internalId == tag.internalId, + orElse: () => tag, + ); + final isUpdated = matchingTag != tag; + return isUpdated + ? tag.copyWith(tag: matchingTag?.tag) + : tag; + }) ?? + [], + ...?matchingEventSubspace.tags?.where( + (e) => + subspace.tags + ?.every((t) => t.internalId != e.internalId) ?? + true, + ) ?? + [], + ]; + return subspace.copyWith( + subspaceName: matchingEventSubspace.subspaceName, + tags: updatedTags, + ); + }).toList() ?? + []; + + // Add new subspaces + event.subspaces + .where((e) => + updatedSubspaces.every((s) => s.internalId != e.internalId)) + .forEach((newSubspace) { + updatedSubspaces.add(newSubspace); + }); + + final updatedSpace = + currentState.space.copyWith(subspaceModels: updatedSubspaces); + + emit(CreateSpaceModelLoaded(updatedSpace)); + } else { + emit(CreateSpaceModelError("Space template not initialized")); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is CreateSpaceModelLoaded) { + final eventTagIds = event.tags.map((e) => e.internalId).toSet(); + + final updatedTags = currentState.space.tags + ?.where((tag) => eventTagIds.contains(tag.internalId)) + .map((tag) { + final matchingEventTag = event.tags.firstWhere( + (e) => e.internalId == tag.internalId, + orElse: () => tag, + ); + return matchingEventTag != tag + ? tag.copyWith(tag: matchingEventTag.tag) + : tag; + }).toList() ?? + []; + + event.tags + .where( + (e) => updatedTags.every((t) => t.internalId != e.internalId)) + .forEach((e) { + updatedTags.add(e); + }); + + emit(CreateSpaceModelLoaded( + currentState.space.copyWith(tags: updatedTags))); + } else { + emit(CreateSpaceModelError("Space template not initialized")); + } + }); + + on((event, emit) { + final currentState = state; + if (currentState is CreateSpaceModelLoaded) { + if (event.allModels.contains(event.name) == true) { + emit(CreateSpaceModelLoaded( + currentState.space, + errorMessage: "Duplicate Model name", + )); + } else if (event.name.trim().isEmpty) { + emit(CreateSpaceModelLoaded( + currentState.space, + errorMessage: "Model name cannot be empty", + )); + } else { + final updatedSpaceModel = + currentState.space.copyWith(modelName: event.name); + + emit(CreateSpaceModelLoaded(updatedSpaceModel)); + } + } else { + emit(CreateSpaceModelError("Space template not initialized")); + } + }); + + on((event, emit) async { + try { + final prevSpaceModel = event.spaceTemplate; + final newSpaceModel = event.updatedSpaceTemplate; + String? spaceModelName; + if (prevSpaceModel.modelName != newSpaceModel.modelName) { + spaceModelName = newSpaceModel.modelName; + } + List tagUpdates = []; + final List subspaceUpdates = []; + final List? prevSubspaces = + prevSpaceModel.subspaceModels; + final List? newSubspaces = + newSpaceModel.subspaceModels; + + tagUpdates = processTagUpdates(prevSpaceModel.tags, newSpaceModel.tags); + + if (prevSubspaces != null || newSubspaces != null) { + if (prevSubspaces != null && newSubspaces != null) { + for (var prevSubspace in prevSubspaces!) { + final existsInNew = newSubspaces! + .any((newTag) => newTag.uuid == prevSubspace.uuid); + if (!existsInNew) { + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.delete, uuid: prevSubspace.uuid)); + } + } + } else if (prevSubspaces != null && newSubspaces == null) { + for (var prevSubspace in prevSubspaces) { + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.delete, uuid: prevSubspace.uuid)); + } + } + + if (newSubspaces != null) { + for (var newSubspace in newSubspaces!) { + // Tag without UUID + if ((newSubspace.uuid == null || newSubspace.uuid!.isEmpty)) { + final List tagUpdates = []; + + if (newSubspace.tags != null) { + for (var tag in newSubspace.tags!) { + tagUpdates.add(TagModelUpdate( + action: Action.add, + tag: tag.tag, + productUuid: tag.product?.uuid)); + } + } + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.add, + subspaceName: newSubspace.subspaceName, + tags: tagUpdates)); + } + } + } + + if (prevSubspaces != null && newSubspaces != null) { + final newSubspaceMap = { + for (var subspace in newSubspaces!) subspace.uuid: subspace + }; + + for (var prevSubspace in prevSubspaces!) { + final newSubspace = newSubspaceMap[prevSubspace.uuid]; + if (newSubspace != null) { + final List tagSubspaceUpdates = + processTagUpdates(prevSubspace.tags, newSubspace.tags); + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.update, + uuid: newSubspace.uuid, + subspaceName: newSubspace.subspaceName, + tags: tagSubspaceUpdates)); + } else {} + } + } + } + + final spaceModelBody = CreateSpaceTemplateBodyModel( + modelName: spaceModelName, + tags: tagUpdates, + subspaceModels: subspaceUpdates); + + final res = await _api.updateSpaceModel( + spaceModelBody, prevSpaceModel.uuid ?? ''); + + if (res != null) { + emit(CreateSpaceModelLoaded(newSpaceModel)); + if (event.onUpdate != null) { + event.onUpdate!(event.updatedSpaceTemplate); + } + } + } catch (e) { + emit(CreateSpaceModelError('Error creating space model')); + } + }); + } + + List processTagUpdates( + List? prevTags, + List? newTags, + ) { + final List tagUpdates = []; + final processedTags = {}; + + if (newTags != null || prevTags != null) { + // Case 1: Tags deleted + if (prevTags != null && newTags != null) { + for (var prevTag in prevTags!) { + final existsInNew = + newTags!.any((newTag) => newTag.uuid == prevTag.uuid); + if (!existsInNew) { + tagUpdates + .add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid)); + } + } + } else if (prevTags != null && newTags == null) { + for (var prevTag in prevTags) { + tagUpdates + .add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid)); + } + } + + // Case 2: Tags added + if (newTags != null) { + for (var newTag in newTags!) { + // Tag without UUID + if ((newTag.uuid == null || newTag.uuid!.isEmpty) && + !processedTags.contains(newTag.tag)) { + tagUpdates.add(TagModelUpdate( + action: Action.add, + tag: newTag.tag, + productUuid: newTag.product?.uuid)); + processedTags.add(newTag.tag); + } + } + } + + // Case 3: Tags updated + if (prevTags != null && newTags != null) { + final newTagMap = {for (var tag in newTags!) tag.uuid: tag}; + + for (var prevTag in prevTags!) { + final newTag = newTagMap[prevTag.uuid]; + if (newTag != null) { + tagUpdates.add(TagModelUpdate( + action: Action.update, + uuid: newTag.uuid, + tag: newTag.tag, + )); + } else {} + } + } + } + + return tagUpdates; + } +} diff --git a/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart b/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart new file mode 100644 index 00000000..22828941 --- /dev/null +++ b/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart @@ -0,0 +1,71 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +abstract class CreateSpaceModelEvent extends Equatable { + const CreateSpaceModelEvent(); + + @override + List get props => []; +} + +class LoadSpaceTemplate extends CreateSpaceModelEvent {} + +class UpdateSpaceTemplate extends CreateSpaceModelEvent { + final SpaceTemplateModel spaceTemplate; + + UpdateSpaceTemplate(this.spaceTemplate); +} + +class CreateSpaceTemplate extends CreateSpaceModelEvent { + final SpaceTemplateModel spaceTemplate; + final Function(SpaceTemplateModel)? onCreate; + + const CreateSpaceTemplate({ + required this.spaceTemplate, + this.onCreate, + }); + + @override + List get props => [spaceTemplate]; +} + +class UpdateSpaceTemplateName extends CreateSpaceModelEvent { + final String name; + final List allModels; + + UpdateSpaceTemplateName({required this.name, required this.allModels}); + + @override + List get props => [name, allModels]; +} + +class AddSubspacesToSpaceTemplate extends CreateSpaceModelEvent { + final List subspaces; + + AddSubspacesToSpaceTemplate(this.subspaces); +} + +class AddTagsToSpaceTemplate extends CreateSpaceModelEvent { + final List tags; + + AddTagsToSpaceTemplate(this.tags); +} + +class ValidateSpaceTemplateName extends CreateSpaceModelEvent { + final String name; + + ValidateSpaceTemplateName({required this.name}); +} + +class ModifySpaceTemplate extends CreateSpaceModelEvent { + final SpaceTemplateModel spaceTemplate; + final SpaceTemplateModel updatedSpaceTemplate; + final Function(SpaceTemplateModel)? onUpdate; + + ModifySpaceTemplate( + {required this.spaceTemplate, + required this.updatedSpaceTemplate, + this.onUpdate}); +} diff --git a/lib/pages/spaces_management/space_model/bloc/create_space_model_state.dart b/lib/pages/spaces_management/space_model/bloc/create_space_model_state.dart new file mode 100644 index 00000000..0fc5c48d --- /dev/null +++ b/lib/pages/spaces_management/space_model/bloc/create_space_model_state.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; + +abstract class CreateSpaceModelState extends Equatable { + const CreateSpaceModelState(); + + @override + List get props => []; +} + +class CreateSpaceModelInitial extends CreateSpaceModelState {} + +class CreateSpaceModelLoading extends CreateSpaceModelState {} + +class CreateSpaceModelLoaded extends CreateSpaceModelState { + final SpaceTemplateModel space; + final String? errorMessage; + + CreateSpaceModelLoaded(this.space, {this.errorMessage}); + + @override + List get props => [space, errorMessage]; +} + +class CreateSpaceModelError extends CreateSpaceModelState { + final String message; + + CreateSpaceModelError(this.message); +} diff --git a/lib/pages/spaces_management/space_model/bloc/space_model_bloc.dart b/lib/pages/spaces_management/space_model/bloc/space_model_bloc.dart new file mode 100644 index 00000000..090dfa13 --- /dev/null +++ b/lib/pages/spaces_management/space_model/bloc/space_model_bloc.dart @@ -0,0 +1,56 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/services/space_model_mang_api.dart'; + +class SpaceModelBloc extends Bloc { + final SpaceModelManagementApi api; + + SpaceModelBloc({ + required this.api, + required List initialSpaceModels, + }) : super(SpaceModelLoaded(spaceModels: initialSpaceModels)) { + on(_onCreateSpaceModel); + on(_onUpdateSpaceModel); + } + + Future _onCreateSpaceModel( + CreateSpaceModel event, Emitter emit) async { + final currentState = state; + if (currentState is SpaceModelLoaded) { + try { + final newSpaceModel = + await api.getSpaceModel(event.newSpaceModel.uuid ?? ''); + + if (newSpaceModel != null) { + final updatedSpaceModels = + List.from(currentState.spaceModels) + ..add(newSpaceModel); + emit(SpaceModelLoaded(spaceModels: updatedSpaceModels)); + } + } catch (e) { + emit(SpaceModelError(message: e.toString())); + } + } + } + + Future _onUpdateSpaceModel( + UpdateSpaceModel event, Emitter emit) async { + final currentState = state; + if (currentState is SpaceModelLoaded) { + try { + final newSpaceModel = + await api.getSpaceModel(event.spaceModelUuid ?? ''); + if (newSpaceModel != null) { + final updatedSpaceModels = currentState.spaceModels.map((model) { + return model.uuid == event.spaceModelUuid ? newSpaceModel : model; + }).toList(); + emit(SpaceModelLoaded(spaceModels: updatedSpaceModels)); + } + } catch (e) { + emit(SpaceModelError(message: e.toString())); + } + } + } +} diff --git a/lib/pages/spaces_management/space_model/bloc/space_model_event.dart b/lib/pages/spaces_management/space_model/bloc/space_model_event.dart new file mode 100644 index 00000000..8f71e611 --- /dev/null +++ b/lib/pages/spaces_management/space_model/bloc/space_model_event.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; + +abstract class SpaceModelEvent extends Equatable { + @override + List get props => []; +} + +class LoadSpaceModels extends SpaceModelEvent {} + +class CreateSpaceModel extends SpaceModelEvent { + final SpaceTemplateModel newSpaceModel; + + CreateSpaceModel({required this.newSpaceModel}); + + @override + List get props => [newSpaceModel]; +} + +class GetSpaceModel extends SpaceModelEvent { + final String spaceModelUuid; + + GetSpaceModel({required this.spaceModelUuid}); + + @override + List get props => [spaceModelUuid]; +} + +class UpdateSpaceModel extends SpaceModelEvent { + final String spaceModelUuid; + + UpdateSpaceModel({required this.spaceModelUuid}); + + @override + List get props => [spaceModelUuid]; +} diff --git a/lib/pages/spaces_management/space_model/bloc/space_model_state.dart b/lib/pages/spaces_management/space_model/bloc/space_model_state.dart new file mode 100644 index 00000000..53adf973 --- /dev/null +++ b/lib/pages/spaces_management/space_model/bloc/space_model_state.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; + +abstract class SpaceModelState extends Equatable { + @override + List get props => []; +} + +class SpaceModelInitial extends SpaceModelState {} + +class SpaceModelLoading extends SpaceModelState {} + +class SpaceModelLoaded extends SpaceModelState { + final List spaceModels; + + SpaceModelLoaded({required this.spaceModels}); + + @override + List get props => [spaceModels]; +} + +class SpaceModelError extends SpaceModelState { + final String message; + + SpaceModelError({required this.message}); + + @override + List get props => [message]; +} diff --git a/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart b/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart new file mode 100644 index 00000000..9b61f1b0 --- /dev/null +++ b/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart @@ -0,0 +1,55 @@ +class TagBodyModel { + late String uuid; + late String tag; + late final String? productUuid; + + Map toJson() { + return { + 'uuid': uuid, + 'tag': tag, + 'productUuid': productUuid, + }; + } + + @override + String toString() { + return toJson().toString(); + } +} + +class CreateSubspaceTemplateModel { + late String subspaceName; + late List? tags; + + Map toJson() { + return { + 'subspaceName': subspaceName, + 'tags': tags?.map((tag) => tag.toJson()).toList(), + }; + } +} + +class CreateSpaceTemplateBodyModel { + final String? modelName; + final List? tags; + final List? subspaceModels; + + CreateSpaceTemplateBodyModel({ + this.modelName, + this.tags, + this.subspaceModels, + }); + + Map toJson() { + return { + 'modelName': modelName, + 'tags': tags, + 'subspaceModels': subspaceModels, + }; + } + + @override + String toString() { + return toJson().toString(); + } +} diff --git a/lib/pages/spaces_management/space_model/models/space_template_model.dart b/lib/pages/spaces_management/space_model/models/space_template_model.dart new file mode 100644 index 00000000..5edf912f --- /dev/null +++ b/lib/pages/spaces_management/space_model/models/space_template_model.dart @@ -0,0 +1,130 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart'; +import 'package:syncrow_web/utils/constants/action_enum.dart'; +import 'package:uuid/uuid.dart'; + +class SpaceTemplateModel extends Equatable { + String? uuid; + String modelName; + List? subspaceModels; + final List? tags; + String internalId; + + @override + List get props => [modelName, subspaceModels, tags]; + + SpaceTemplateModel({ + this.uuid, + String? internalId, + required this.modelName, + this.subspaceModels, + this.tags, + }) : internalId = internalId ?? const Uuid().v4(); + + factory SpaceTemplateModel.fromJson(Map json) { + final String internalId = json['internalId'] ?? const Uuid().v4(); + + return SpaceTemplateModel( + uuid: json['uuid'] ?? '', + internalId: internalId, + modelName: json['modelName'] ?? '', + subspaceModels: (json['subspaceModels'] as List?) + ?.where((e) => e is Map) // Validate type + .map((e) => + SubspaceTemplateModel.fromJson(e as Map)) + .toList() ?? + [], + tags: (json['tags'] as List?) + ?.where((item) => item is Map) // Validate type + .map((item) => TagModel.fromJson(item as Map)) + .toList() ?? + [], + ); + } + SpaceTemplateModel copyWith({ + String? uuid, + String? modelName, + List? subspaceModels, + List? tags, + String? internalId, + }) { + return SpaceTemplateModel( + uuid: uuid ?? this.uuid, + modelName: modelName ?? this.modelName, + subspaceModels: subspaceModels ?? this.subspaceModels, + tags: tags ?? this.tags, + internalId: internalId ?? this.internalId, + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'modelName': modelName, + 'subspaceModels': subspaceModels?.map((e) => e.toJson()).toList(), + 'tags': tags?.map((e) => e.toJson()).toList(), + }; + } +} + +class UpdateSubspaceTemplateModel { + final String? uuid; + final Action action; + final String? subspaceName; + final List? tags; + + UpdateSubspaceTemplateModel({ + required this.action, + this.uuid, + this.subspaceName, + this.tags, + }); + + factory UpdateSubspaceTemplateModel.fromJson(Map json) { + return UpdateSubspaceTemplateModel( + action: ActionExtension.fromValue(json['action']), + uuid: json['uuid'] ?? '', + subspaceName: json['subspaceName'] ?? '', + tags: (json['tags'] as List) + .map((item) => TagModelUpdate.fromJson(item)) + .toList(), + ); + } + + Map toJson() { + return { + 'action': action.value, + 'uuid': uuid, + 'subspaceName': subspaceName, + 'tags': tags?.map((e) => e.toJson()).toList() ?? [], + }; + } +} + +extension SpaceTemplateExtensions on SpaceTemplateModel { + List listAllTagValues() { + final List tagValues = []; + + if (tags != null) { + tagValues.addAll( + tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty)); + } + + if (subspaceModels != null) { + for (final subspace in subspaceModels!) { + if (subspace.tags != null) { + tagValues.addAll( + subspace.tags! + .map((tag) => tag.tag ?? '') + .where((tag) => tag.isNotEmpty), + ); + } + } + } + + return tagValues; + } +} diff --git a/lib/pages/spaces_management/space_model/models/subspace_template_model.dart b/lib/pages/spaces_management/space_model/models/subspace_template_model.dart new file mode 100644 index 00000000..6c73741b --- /dev/null +++ b/lib/pages/spaces_management/space_model/models/subspace_template_model.dart @@ -0,0 +1,58 @@ +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:uuid/uuid.dart'; + +class SubspaceTemplateModel { + final String? uuid; + String subspaceName; + final bool disabled; + List? tags; + String internalId; + + SubspaceTemplateModel({ + this.uuid, + required this.subspaceName, + required this.disabled, + this.tags, + String? internalId, + }) : internalId = internalId ?? const Uuid().v4(); + + factory SubspaceTemplateModel.fromJson(Map json) { + final String internalId = json['internalId'] ?? const Uuid().v4(); + + return SubspaceTemplateModel( + uuid: json['uuid'] ?? '', + subspaceName: json['subspaceName'] ?? '', + internalId: internalId, + disabled: json['disabled'] ?? false, + tags: (json['tags'] as List?) + ?.map((item) => TagModel.fromJson(item)) + .toList() ?? + [], + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'subspaceName': subspaceName, + 'disabled': disabled, + 'tags': tags?.map((e) => e.toJson()).toList() ?? [], + }; + } + + SubspaceTemplateModel copyWith({ + String? uuid, + String? subspaceName, + bool? disabled, + List? tags, + String? internalId, + }) { + return SubspaceTemplateModel( + uuid: uuid ?? this.uuid, + subspaceName: subspaceName ?? this.subspaceName, + disabled: disabled ?? this.disabled, + tags: tags ?? this.tags, + internalId: internalId ?? this.internalId, + ); + } +} diff --git a/lib/pages/spaces_management/space_model/models/tag_body_model.dart b/lib/pages/spaces_management/space_model/models/tag_body_model.dart new file mode 100644 index 00000000..d66e2884 --- /dev/null +++ b/lib/pages/spaces_management/space_model/models/tag_body_model.dart @@ -0,0 +1,16 @@ +class CreateTagBodyModel { + late String tag; + late final String? productUuid; + + Map toJson() { + return { + 'tag': tag, + 'productUuid': productUuid, + }; + } + + @override + String toString() { + return toJson().toString(); + } +} diff --git a/lib/pages/spaces_management/space_model/models/tag_model.dart b/lib/pages/spaces_management/space_model/models/tag_model.dart new file mode 100644 index 00000000..48f89167 --- /dev/null +++ b/lib/pages/spaces_management/space_model/models/tag_model.dart @@ -0,0 +1,62 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; +import 'package:uuid/uuid.dart'; + +class TagModel { + String? uuid; + String? tag; + final ProductModel? product; + String internalId; + String? location; + + TagModel( + {this.uuid, + required this.tag, + this.product, + String? internalId, + this.location}) + : internalId = internalId ?? const Uuid().v4(); + + factory TagModel.fromJson(Map json) { + final String internalId = json['internalId'] ?? const Uuid().v4(); + + return TagModel( + uuid: json['uuid'] ?? '', + internalId: internalId, + tag: json['tag'] ?? '', + product: json['product'] != null + ? ProductModel.fromMap(json['product']) + : null, + ); + } + + TagModel copyWith( + {String? tag, + ProductModel? product, + String? location, + String? internalId}) { + return TagModel( + tag: tag ?? this.tag, + product: product ?? this.product, + location: location ?? this.location, + internalId: internalId ?? this.internalId, + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'tag': tag, + 'product': product?.toMap(), + }; + } +} + +extension TagModelExtensions on TagModel { + TagBodyModel toTagBodyModel() { + return TagBodyModel() + ..uuid = uuid ?? '' + ..tag = tag ?? '' + ..productUuid = product?.uuid; + } +} diff --git a/lib/pages/spaces_management/space_model/models/tag_update_model.dart b/lib/pages/spaces_management/space_model/models/tag_update_model.dart new file mode 100644 index 00000000..c7190dc8 --- /dev/null +++ b/lib/pages/spaces_management/space_model/models/tag_update_model.dart @@ -0,0 +1,34 @@ +import 'package:syncrow_web/utils/constants/action_enum.dart'; + +class TagModelUpdate { + final Action action; + final String? uuid; + final String? tag; + final String? productUuid; + + TagModelUpdate({ + required this.action, + this.uuid, + this.tag, + this.productUuid, + }); + + factory TagModelUpdate.fromJson(Map json) { + return TagModelUpdate( + action: json['action'], + uuid: json['uuid'], + tag: json['tag'], + productUuid: json['productUuid'], + ); + } + + // Method to convert an instance to JSON + Map toJson() { + return { + 'action': action.value, + 'uuid': uuid, // Nullable field + 'tag': tag, + 'productUuid': productUuid, + }; + } +} diff --git a/lib/pages/spaces_management/space_model/view/space_model_page.dart b/lib/pages/spaces_management/space_model/view/space_model_page.dart new file mode 100644 index 00000000..ae623e81 --- /dev/null +++ b/lib/pages/spaces_management/space_model/view/space_model_page.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/add_space_model_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceModelPage extends StatelessWidget { + final List? products; + + const SpaceModelPage({Key? key, this.products}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is SpaceModelLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is SpaceModelLoaded) { + final spaceModels = state.spaceModels; + final allTagValues = _getAllTagValues(spaceModels); + final allSpaceModelNames = _getAllSpaceModelName(spaceModels); + + return Scaffold( + backgroundColor: ColorsManager.whiteColors, + body: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 16.0, 16.0, 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + childAspectRatio: _calculateChildAspectRatio(context), + ), + itemCount: spaceModels.length + 1, + itemBuilder: (context, index) { + if (index == spaceModels.length) { + // Add Button + return GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return CreateSpaceModelDialog( + products: products, + allTags: allTagValues, + pageContext: context, + otherSpaceModels: allSpaceModelNames, + ); + }, + ); + }, + child: const AddSpaceModelWidget(), + ); + } + // Render existing space model + final model = spaceModels[index]; + final otherModel = List.from(allSpaceModelNames); + otherModel.remove(model.modelName); + return GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return CreateSpaceModelDialog( + products: products, + allTags: allTagValues, + spaceModel: model, + otherSpaceModels: otherModel, + pageContext: context, + ); + }, + ); + }, + child: Container( + margin: const EdgeInsets.all(8.0), + child: SpaceModelCardWidget(model: model), + )); + }, + ), + ), + ], + ), + ), + ); + } else if (state is SpaceModelError) { + return Center( + child: Text( + 'Error: ${state.message}', + style: const TextStyle(color: ColorsManager.warningRed), + ), + ); + } + return const Center(child: Text('Initializing...')); + }, + ); + } + + double _calculateChildAspectRatio(BuildContext context) { + double screenWidth = MediaQuery.of(context).size.width; + if (screenWidth > 1600) { + return 2; + } + if (screenWidth > 1200) { + return 3; + } else if (screenWidth > 800) { + return 3.5; + } else { + return 4.0; + } + } + + List _getAllTagValues(List spaceModels) { + final List allTags = []; + for (final spaceModel in spaceModels) { + if (spaceModel.tags != null) { + allTags.addAll(spaceModel.listAllTagValues()); + } + } + return allTags; + } + + List _getAllSpaceModelName(List spaceModels) { + final List names = []; + for (final spaceModel in spaceModels) { + names.add(spaceModel.modelName); + } + return names; + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/add_space_model_widget.dart b/lib/pages/spaces_management/space_model/widgets/add_space_model_widget.dart new file mode 100644 index 00000000..20876a39 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/add_space_model_widget.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AddSpaceModelWidget extends StatelessWidget { + const AddSpaceModelWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: ColorsManager.semiTransparentBlackColor, + blurRadius: 15, + offset: Offset(0, 4), + spreadRadius: 0, + ), + BoxShadow( + color: ColorsManager.semiTransparentBlackColor, + blurRadius: 25, + offset: Offset(0, 15), + spreadRadius: -5, + ), + ], + ), + child: Center( + child: Container( + width: 60, + height: 60, // Set a proper height here + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.neutralGray, + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 2.0, + ), + ), + child: const Icon( + Icons.add, + size: 40, + color: ColorsManager.spaceColor, + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart b/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart new file mode 100644 index 00000000..81ecb674 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ButtonContentWidget extends StatelessWidget { + final IconData icon; + final String label; + + const ButtonContentWidget({ + Key? key, + required this.icon, + required this.label, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return SizedBox( + width: screenWidth * 0.25, + child: Container( + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + border: Border.all( + color: ColorsManager.neutralGray, + width: 3.0, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Row( + children: [ + Icon( + icon, + color: ColorsManager.spaceColor, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle( + color: ColorsManager.blackColor, + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart b/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart new file mode 100644 index 00000000..c1bea0fd --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_event.dart'; + +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart'; +import 'package:syncrow_web/services/space_model_mang_api.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSpaceModelDialog extends StatelessWidget { + final List? products; + final List? allTags; + final SpaceTemplateModel? spaceModel; + final BuildContext? pageContext; + final List? otherSpaceModels; + + const CreateSpaceModelDialog( + {Key? key, + this.products, + this.allTags, + this.spaceModel, + this.pageContext, + this.otherSpaceModels}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi(); + + final screenWidth = MediaQuery.of(context).size.width; + final TextEditingController spaceNameController = TextEditingController( + text: spaceModel?.modelName ?? '', + ); + + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + width: screenWidth * 0.3, + child: BlocProvider( + create: (_) { + final bloc = CreateSpaceModelBloc(_spaceModelApi); + if (spaceModel != null) { + bloc.add(UpdateSpaceTemplate(spaceModel!)); + } else { + bloc.add(UpdateSpaceTemplate(SpaceTemplateModel( + modelName: '', + subspaceModels: const [], + ))); + } + + spaceNameController.addListener(() { + bloc.add(UpdateSpaceTemplateName( + name: spaceNameController.text, + allModels: otherSpaceModels ?? [])); + }); + + return bloc; + }, + child: BlocBuilder( + builder: (context, state) { + if (state is CreateSpaceModelLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is CreateSpaceModelLoaded) { + final updatedSpaceModel = state.space; + final subspaces = updatedSpaceModel.subspaceModels ?? []; + final isNameValid = spaceNameController.text.trim().isNotEmpty; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + spaceModel?.uuid == null + ? 'Create New Space Model' + : 'Edit Space Model', + style: Theme.of(context) + .textTheme + .headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + const SizedBox(height: 16), + SizedBox( + width: screenWidth * 0.25, + child: TextField( + controller: spaceNameController, + onChanged: (value) { + context.read().add( + UpdateSpaceTemplateName( + name: value, + allModels: otherSpaceModels ?? [])); + }, + style: const TextStyle(color: ColorsManager.blackColor), + decoration: InputDecoration( + filled: true, + fillColor: ColorsManager.textFieldGreyColor, + hintText: 'Please enter the name', + errorText: state.errorMessage, + hintStyle: const TextStyle( + color: ColorsManager.lightGrayColor), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 16.0, + ), + ), + ), + ), + const SizedBox(height: 16), + SubspaceModelCreate( + context, + subspaces: state.space.subspaceModels ?? [], + onSpaceModelUpdate: (updatedSubspaces) { + context + .read() + .add(AddSubspacesToSpaceTemplate(updatedSubspaces)); + }, + ), + const SizedBox(height: 10), + TagChipDisplay( + context, + screenWidth: screenWidth, + spaceModel: updatedSpaceModel, + products: products, + subspaces: subspaces, + allTags: allTags, + spaceNameController: spaceNameController, + pageContext: pageContext, + otherSpaceModels: otherSpaceModels, + ), + const SizedBox(height: 20), + SizedBox( + width: screenWidth * 0.25, + child: Row( + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: state.errorMessage == null || + isNameValid + ? () { + final updatedSpaceTemplate = + updatedSpaceModel.copyWith( + modelName: + spaceNameController.text.trim(), + ); + if (updatedSpaceModel.uuid == null) { + context + .read() + .add( + CreateSpaceTemplate( + spaceTemplate: + updatedSpaceTemplate, + onCreate: (newModel) { + if (pageContext != null) { + pageContext! + .read() + .add(CreateSpaceModel( + newSpaceModel: + newModel)); + } + Navigator.of(context) + .pop(); // Close the dialog + }, + ), + ); + } else { + if (pageContext != null) { + final currentState = pageContext! + .read() + .state; + if (currentState + is SpaceModelLoaded) { + final spaceModels = + List.from( + currentState.spaceModels); + + final SpaceTemplateModel? + currentSpaceModel = spaceModels + .cast() + .firstWhere( + (sm) => + sm?.uuid == + updatedSpaceModel + .uuid, + orElse: () => null, + ); + if (currentSpaceModel != null) { + context + .read() + .add(ModifySpaceTemplate( + spaceTemplate: + currentSpaceModel, + updatedSpaceTemplate: + updatedSpaceTemplate, + onUpdate: (newModel) { + if (pageContext != + null) { + pageContext! + .read< + SpaceModelBloc>() + .add(UpdateSpaceModel( + spaceModelUuid: + newModel.uuid ?? + '')); + } + Navigator.of(context) + .pop(); + })); + } + } + } + } + } + : null, + backgroundColor: ColorsManager.secondaryColor, + borderRadius: 10, + foregroundColor: isNameValid + ? ColorsManager.whiteColors + : ColorsManager.whiteColorsWithOpacity, + child: const Text('OK'), + ), + ), + ], + ), + ), + ], + ); + } else if (state is CreateSpaceModelError) { + return Text( + 'Error: ${state.message}', + style: const TextStyle(color: ColorsManager.warningRed), + ); + } + + return const Center(child: Text('Initializing...')); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart b/lib/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart new file mode 100644 index 00000000..4f42e3bf --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/flexible_item_widget.dart'; + +import 'package:syncrow_web/utils/color_manager.dart'; + +class DynamicProductWidget extends StatelessWidget { + final Map productTagCount; + final double maxWidth; + final double maxHeight; + + const DynamicProductWidget({ + Key? key, + required this.productTagCount, + required this.maxWidth, + required this.maxHeight, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + const double itemSpacing = 8.0; + const double lineSpacing = 8.0; + const double textPadding = 16.0; + const double itemHeight = 40.0; + + List productWidgets = []; + double currentLineWidth = 0.0; + double currentHeight = itemHeight; + + for (final product in productTagCount.entries) { + final String prodType = product.key; + final int count = product.value; + + final TextPainter textPainter = TextPainter( + text: TextSpan( + text: 'x$count', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + textDirection: TextDirection.ltr, + )..layout(); + + final double itemWidth = textPainter.width + textPadding + 20; + + if (currentLineWidth + itemWidth + itemSpacing > maxWidth) { + currentHeight += itemHeight + lineSpacing; + if (currentHeight > maxHeight) { + productWidgets.add(const EllipsisItemWidget()); + break; + } + currentLineWidth = 0.0; + } + + productWidgets.add(FlexibleItemWidget(prodType: prodType, count: count)); + currentLineWidth += itemWidth + itemSpacing; + } + + return Wrap( + spacing: itemSpacing, + runSpacing: lineSpacing, + children: productWidgets, + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart b/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart new file mode 100644 index 00000000..e24c7704 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/room_name_widget.dart'; + +class DynamicRoomWidget extends StatelessWidget { + final List? subspaceModels; + final double maxWidth; + final double maxHeight; + + const DynamicRoomWidget({ + Key? key, + required this.subspaceModels, + required this.maxWidth, + required this.maxHeight, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + const double itemSpacing = 8.0; + const double lineSpacing = 8.0; + const double textPadding = 16.0; + const double itemHeight = 30.0; + + List roomWidgets = []; + double currentLineWidth = 0.0; + double currentHeight = itemHeight; + + for (final subspace in subspaceModels!) { + final TextPainter textPainter = TextPainter( + text: TextSpan( + text: subspace.subspaceName, + style: const TextStyle(fontSize: 16), + ), + textDirection: TextDirection.ltr, + )..layout(); + + final double itemWidth = textPainter.width + textPadding; + + if (currentLineWidth + itemWidth + itemSpacing > maxWidth) { + currentHeight += itemHeight + lineSpacing; + if (currentHeight > maxHeight) { + roomWidgets.add(const RoomNameWidget(name: "...")); + break; + } + currentLineWidth = 0.0; + } + + roomWidgets.add(RoomNameWidget(name: subspace.subspaceName)); + currentLineWidth += itemWidth + itemSpacing; + } + + return Wrap( + spacing: itemSpacing, + runSpacing: lineSpacing, + children: roomWidgets, + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart b/lib/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart new file mode 100644 index 00000000..7ede09a7 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EllipsisItemWidget extends StatelessWidget { + const EllipsisItemWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorsManager.transparentColor), + ), + child: Text( + "...", + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/flexible_item_widget.dart b/lib/pages/spaces_management/space_model/widgets/flexible_item_widget.dart new file mode 100644 index 00000000..c28a82b8 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/flexible_item_widget.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class FlexibleItemWidget extends StatelessWidget { + final String prodType; + final int count; + + const FlexibleItemWidget({ + Key? key, + required this.prodType, + required this.count, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorsManager.transparentColor), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + prodType, + width: 15, + height: 16, + ), + const SizedBox(width: 4), + Text( + 'x$count', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + ], + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/room_name_widget.dart b/lib/pages/spaces_management/space_model/widgets/room_name_widget.dart new file mode 100644 index 00000000..d59f8c1e --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/room_name_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoomNameWidget extends StatelessWidget { + final String name; + + const RoomNameWidget({Key? key, required this.name}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorsManager.transparentColor), + ), + child: Text( + name, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart b/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart new file mode 100644 index 00000000..df0fba4f --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceModelCardWidget extends StatelessWidget { + final SpaceTemplateModel model; + + const SpaceModelCardWidget({Key? key, required this.model}) : super(key: key); + + @override + Widget build(BuildContext context) { + final Map productTagCount = {}; + + if (model.tags != null) { + for (var tag in model.tags!) { + final prodIcon = tag.product?.icon ?? 'Unknown'; + productTagCount[prodIcon] = (productTagCount[prodIcon] ?? 0) + 1; + } + } + + if (model.subspaceModels != null) { + for (var subspace in model.subspaceModels!) { + if (subspace.tags != null) { + for (var tag in subspace.tags!) { + final prodIcon = tag.product?.icon ?? 'Unknown'; + productTagCount[prodIcon] = (productTagCount[prodIcon] ?? 0) + 1; + } + } + } + } + + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.modelName, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Expanded( + child: Row( + children: [ + // Left Container + Expanded( + flex: 1, // Distribute space proportionally + child: Container( + padding: const EdgeInsets.all(8.0), + child: LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Alignment.topLeft, + child: DynamicRoomWidget( + subspaceModels: model.subspaceModels, + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, + ), + ); + }, + ), + ), + ), + if (productTagCount.isNotEmpty && model.subspaceModels != null) + Container( + width: 1.0, + color: ColorsManager.softGray, + margin: const EdgeInsets.symmetric(vertical: 6.0), + ), + Expanded( + flex: 1, // Distribute space proportionally + child: Container( + padding: const EdgeInsets.all(8.0), + child: LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Alignment.topLeft, + child: DynamicProductWidget( + productTagCount: productTagCount, + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight)); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/subspace_chip_widget.dart b/lib/pages/spaces_management/space_model/widgets/subspace_chip_widget.dart new file mode 100644 index 00000000..70ac6e24 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/subspace_chip_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SubspaceChipWidget extends StatelessWidget { + final String subspace; + + const SubspaceChipWidget({ + Key? key, + required this.subspace, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ColorsManager.transparentColor, + width: 0, + ), + ), + child: Text( + subspace, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart b/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart new file mode 100644 index 00000000..0dda53a6 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SubspaceModelCreate extends StatelessWidget { + final List subspaces; + final void Function(List newSubspaces)? + onSpaceModelUpdate; + + const SubspaceModelCreate(BuildContext context, + {Key? key, required this.subspaces, this.onSpaceModelUpdate}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + return Container( + child: subspaces.isEmpty + ? TextButton( + style: TextButton.styleFrom( + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () async { + await _openDialog(context, 'Create Sub-space'); + }, + child: const ButtonContentWidget( + icon: Icons.add, + label: 'Create Sub Space', + ), + ) + : SizedBox( + width: screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map((subspace) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: ColorsManager.transparentColor), + ), + child: Text( + subspace.subspaceName, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + )), + GestureDetector( + onTap: () async { + await _openDialog(context, 'Edit Sub-space'); + }, + child: Chip( + label: const Text( + 'Edit', + style: TextStyle(color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: + const BorderSide(color: ColorsManager.spaceColor), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Future _openDialog(BuildContext context, String dialogTitle) async { + await showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + return CreateSubSpaceModelDialog( + isEdit: true, + dialogTitle: dialogTitle, + existingSubSpaces: subspaces, + onUpdate: (subspaceModels) { + onSpaceModelUpdate!(subspaceModels); + }, + ); + }, + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart b/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart new file mode 100644 index 00000000..d4111031 --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class TagChipDisplay extends StatelessWidget { + final double screenWidth; + final SpaceTemplateModel? spaceModel; + final List? products; + final List? subspaces; + final List? allTags; + final TextEditingController spaceNameController; + final BuildContext? pageContext; + final List? otherSpaceModels; + + const TagChipDisplay(BuildContext context, + {Key? key, + required this.screenWidth, + required this.spaceModel, + required this.products, + required this.subspaces, + required this.allTags, + required this.spaceNameController, + this.pageContext, + this.otherSpaceModels}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return (spaceModel?.tags?.isNotEmpty == true || + spaceModel?.subspaceModels + ?.any((subspace) => subspace.tags?.isNotEmpty == true) == + true) + ? SizedBox( + width: screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + ...TagHelper.groupTags([ + ...?spaceModel?.tags, + ...?spaceModel?.subspaceModels + ?.expand((subspace) => subspace.tags ?? []) + ]).entries.map( + (entry) => Chip( + avatar: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + entry.key.icon ?? 'assets/icons/gateway.svg', + fit: BoxFit.contain, + ), + ), + label: Text( + 'x${entry.value}', // Show count + style: const TextStyle( + color: ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor, + ), + ), + ), + ), + GestureDetector( + onTap: () async { + // Use the Navigator's context for showDialog + final navigatorContext = + Navigator.of(context).overlay?.context; + + if (navigatorContext != null) { + await showDialog( + barrierDismissible: false, + context: navigatorContext, + builder: (context) => AssignTagModelsDialog( + products: products, + subspaces: subspaces, + pageContext: pageContext, + allTags: allTags, + spaceModel: spaceModel, + initialTags: TagHelper.generateInitialTags( + subspaces: subspaces, + spaceTagModels: spaceModel?.tags ?? []), + title: 'Edit Device', + addedProducts: + TagHelper.createInitialSelectedProducts( + spaceModel?.tags ?? [], subspaces), + spaceName: spaceModel?.modelName ?? '', + )); + } + }, + child: Chip( + label: const Text( + 'Edit', + style: TextStyle(color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: ColorsManager.spaceColor), + ), + ), + ), + ], + ), + ), + ) + : TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + await showDialog( + barrierDismissible: false, + context: context, + builder: (context) => AddDeviceTypeModelWidget( + products: products, + subspaces: subspaces, + allTags: allTags, + spaceName: spaceNameController.text, + pageContext: pageContext, + isCreate: true, + spaceModel: spaceModel, + ), + ); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const ButtonContentWidget( + icon: Icons.add, + label: 'Add Devices', + ), + ); + } +} diff --git a/lib/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart b/lib/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart new file mode 100644 index 00000000..1de2ae13 --- /dev/null +++ b/lib/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart @@ -0,0 +1,20 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart'; +import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_state.dart'; + +// Bloc Implementation +class CenterBodyBloc extends Bloc { + CenterBodyBloc() : super(InitialState()) { + on((event, emit) { + emit(CommunityStructureState()); + }); + + on((event, emit) { + emit(SpaceModelState()); + }); + + on((event, emit) { + emit(CommunitySelectedState()); + }); + } +} diff --git a/lib/pages/spaces_management/structure_selector/bloc/center_body_event.dart b/lib/pages/spaces_management/structure_selector/bloc/center_body_event.dart new file mode 100644 index 00000000..72cdbd1c --- /dev/null +++ b/lib/pages/spaces_management/structure_selector/bloc/center_body_event.dart @@ -0,0 +1,8 @@ +// Define Events +abstract class CenterBodyEvent {} + +class CommunityStructureSelectedEvent extends CenterBodyEvent {} + +class SpaceModelSelectedEvent extends CenterBodyEvent {} + +class CommunitySelectedEvent extends CenterBodyEvent {} \ No newline at end of file diff --git a/lib/pages/spaces_management/structure_selector/bloc/center_body_state.dart b/lib/pages/spaces_management/structure_selector/bloc/center_body_state.dart new file mode 100644 index 00000000..73428dc5 --- /dev/null +++ b/lib/pages/spaces_management/structure_selector/bloc/center_body_state.dart @@ -0,0 +1,9 @@ +abstract class CenterBodyState {} + +class InitialState extends CenterBodyState {} + +class CommunityStructureState extends CenterBodyState {} + +class SpaceModelState extends CenterBodyState {} + +class CommunitySelectedState extends CenterBodyState {} diff --git a/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart b/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart new file mode 100644 index 00000000..45a6aaf7 --- /dev/null +++ b/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; +import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart'; +import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_state.dart'; +import '../bloc/center_body_bloc.dart'; + +class CenterBodyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is InitialState) { + context.read().add(CommunityStructureSelectedEvent()); + } + if (state is CommunityStructureState) { + context.read().add(BlankStateEvent()); + } + + if (state is SpaceModelState) { + context.read().add(SpaceModelLoadEvent()); + } + + return Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + context.read().add(CommunityStructureSelectedEvent()); + }, + child: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: state is CommunityStructureState || state is CommunitySelectedState + ? FontWeight.bold + : FontWeight.normal, + color: state is CommunityStructureState || state is CommunitySelectedState + ? Theme.of(context).textTheme.bodyLarge!.color + : Theme.of(context) + .textTheme + .bodyLarge! + .color! + .withOpacity(0.5), + ), + ), + ), + const SizedBox(width: 20), + GestureDetector( + onTap: () { + context.read().add(SpaceModelSelectedEvent()); + }, + child: Text( + 'Space Model', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: state is SpaceModelState + ? FontWeight.bold + : FontWeight.normal, + color: state is SpaceModelState + ? Theme.of(context).textTheme.bodyLarge!.color + : Theme.of(context) + .textTheme + .bodyLarge! + .color! + .withOpacity(0.5), + ), + ), + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart b/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart new file mode 100644 index 00000000..9c617a12 --- /dev/null +++ b/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart @@ -0,0 +1,75 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart'; + +class AddDeviceTypeModelBloc + extends Bloc { + AddDeviceTypeModelBloc() : super(AddDeviceModelInitial()) { + on(_onInitializeTagModels); + on(_onUpdateProductCount); + } + + void _onInitializeTagModels( + InitializeDeviceTypeModel event, Emitter emit) { + emit(AddDeviceModelLoaded( + selectedProducts: event.addedProducts, + initialTag: event.initialTags, + )); + } + + void _onUpdateProductCount( + UpdateProductCountEvent event, Emitter emit) { + final currentState = state; + + if (currentState is AddDeviceModelLoaded) { + final existingProduct = currentState.selectedProducts.firstWhere( + (p) => p.productId == event.productId, + orElse: () => SelectedProduct( + productId: event.productId, + count: 0, + productName: event.productName, + product: event.product, + ), + ); + + List updatedProducts; + + if (event.count > 0) { + if (!currentState.selectedProducts.contains(existingProduct)) { + updatedProducts = [ + ...currentState.selectedProducts, + SelectedProduct( + productId: event.productId, + count: event.count, + productName: event.productName, + product: event.product, + ), + ]; + } else { + updatedProducts = currentState.selectedProducts.map((p) { + if (p.productId == event.productId) { + return SelectedProduct( + productId: p.productId, + count: event.count, + productName: p.productName, + product: p.product, + ); + } + return p; + }).toList(); + } + } else { + // Remove the product if the count is 0 + updatedProducts = currentState.selectedProducts + .where((p) => p.productId != event.productId) + .toList(); + } + + // Emit the updated state + emit(AddDeviceModelLoaded( + selectedProducts: updatedProducts, + initialTag: currentState.initialTag)); + } + } +} diff --git a/lib/pages/spaces_management/tag_model/bloc/add_device_model_state.dart b/lib/pages/spaces_management/tag_model/bloc/add_device_model_state.dart new file mode 100644 index 00000000..f45471cd --- /dev/null +++ b/lib/pages/spaces_management/tag_model/bloc/add_device_model_state.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +abstract class AddDeviceModelState extends Equatable { + const AddDeviceModelState(); + + @override + List get props => []; +} + +class AddDeviceModelInitial extends AddDeviceModelState {} + +class AddDeviceModelLoading extends AddDeviceModelState {} + +class AddDeviceModelLoaded extends AddDeviceModelState { + final List selectedProducts; + final List initialTag; + + const AddDeviceModelLoaded({ + required this.selectedProducts, + required this.initialTag, + }); + + @override + List get props => [selectedProducts, initialTag]; +} + +class AddDeviceModelError extends AddDeviceModelState { + final String errorMessage; + + const AddDeviceModelError(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart b/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart new file mode 100644 index 00000000..9b3a8b1e --- /dev/null +++ b/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +abstract class AddDeviceTypeModelEvent extends Equatable { + const AddDeviceTypeModelEvent(); + + @override + List get props => []; +} + + +class UpdateProductCountEvent extends AddDeviceTypeModelEvent { + final String productId; + final int count; + final String productName; + final ProductModel product; + + UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product}); + + @override + List get props => [productId, count]; +} + + +class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent { + final List initialTags; + final List addedProducts; + + const InitializeDeviceTypeModel({ + this.initialTags = const [], + required this.addedProducts, + }); + + @override + List get props => [initialTags, addedProducts]; +} diff --git a/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart b/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart new file mode 100644 index 00000000..a9d40147 --- /dev/null +++ b/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/scrollable_grid_view_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AddDeviceTypeModelWidget extends StatelessWidget { + final List? products; + final List? initialSelectedProducts; + final List? subspaces; + final List? spaceTagModels; + final List? allTags; + final String spaceName; + final bool isCreate; + final List? otherSpaceModels; + final BuildContext? pageContext; + final SpaceTemplateModel? spaceModel; + + const AddDeviceTypeModelWidget( + {super.key, + this.products, + this.initialSelectedProducts, + this.subspaces, + this.allTags, + this.spaceTagModels, + required this.spaceName, + required this.isCreate, + this.pageContext, + this.otherSpaceModels, + this.spaceModel}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final crossAxisCount = size.width > 1200 + ? 8 + : size.width > 800 + ? 5 + : 3; + + return BlocProvider( + create: (_) => AddDeviceTypeModelBloc() + ..add(InitializeDeviceTypeModel( + initialTags: spaceTagModels ?? [], + addedProducts: initialSelectedProducts ?? [], + )), + child: Builder( + builder: (context) => AlertDialog( + title: const Text('Add Devices'), + backgroundColor: ColorsManager.whiteColors, + content: BlocBuilder( + builder: (context, state) { + if (state is AddDeviceModelLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is AddDeviceModelLoaded) { + return SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + color: ColorsManager.textFieldGreyColor, + child: Column( + children: [ + const SizedBox(height: 16), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20.0), + child: ScrollableGridViewWidget( + isCreate: isCreate, + products: products, + crossAxisCount: crossAxisCount, + initialProductCounts: state.selectedProducts, + ), + ), + ), + ], + ), + ), + ); + } + return const SizedBox(); + }, + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CancelButton( + label: 'Cancel', + onPressed: () async { + if (isCreate) { + Navigator.of(context).pop(); + await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return CreateSpaceModelDialog( + products: products, + allTags: allTags, + pageContext: pageContext, + otherSpaceModels: otherSpaceModels, + spaceModel: SpaceTemplateModel( + modelName: spaceName, + tags: spaceModel?.tags ?? [], + uuid: spaceModel?.uuid, + internalId: spaceModel?.internalId, + subspaceModels: subspaces), + ); + }, + ); + } else { + final initialTags = generateInitialTags( + spaceTagModels: spaceTagModels, + subspaces: subspaces, + ); + + Navigator.of(context).pop(); + await showDialog( + context: context, + builder: (context) => AssignTagModelsDialog( + products: products, + subspaces: subspaces, + addedProducts: initialSelectedProducts ?? [], + allTags: allTags, + spaceName: spaceName, + initialTags: initialTags, + otherSpaceModels: otherSpaceModels, + title: 'Edit Device', + spaceModel: spaceModel, + pageContext: pageContext, + )); + } + }, + ), + SizedBox( + width: 140, + child: + BlocBuilder( + builder: (context, state) { + final isDisabled = state is AddDeviceModelLoaded && + state.selectedProducts.isEmpty; + + return DefaultButton( + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: isDisabled + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, + borderRadius: 10, + onPressed: isDisabled + ? null // Disable the button + : () async { + if (state is AddDeviceModelLoaded && + state.selectedProducts.isNotEmpty) { + final initialTags = generateInitialTags( + spaceTagModels: spaceTagModels, + subspaces: subspaces, + ); + + final dialogTitle = initialTags.isNotEmpty + ? 'Edit Device' + : 'Assign Tags'; + Navigator.of(context).pop(); + await showDialog( + context: context, + builder: (context) => AssignTagModelsDialog( + products: products, + subspaces: subspaces, + addedProducts: state.selectedProducts, + allTags: allTags, + spaceName: spaceName, + initialTags: state.initialTag, + otherSpaceModels: otherSpaceModels, + title: dialogTitle, + spaceModel: spaceModel, + pageContext: pageContext, + ), + ); + } + }, + child: const Text('Next'), + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + List generateInitialTags({ + List? spaceTagModels, + List? subspaces, + }) { + final List initialTags = []; + + if (spaceTagModels != null) { + initialTags.addAll(spaceTagModels); + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + initialTags.addAll( + subspace.tags!.map( + (tag) => tag.copyWith(location: subspace.subspaceName), + ), + ); + } + } + } + + return initialTags; + } +} diff --git a/lib/pages/spaces_management/tag_model/widgets/action_button_widget.dart b/lib/pages/spaces_management/tag_model/widgets/action_button_widget.dart new file mode 100644 index 00000000..8d10a4de --- /dev/null +++ b/lib/pages/spaces_management/tag_model/widgets/action_button_widget.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; + +class ActionButton extends StatelessWidget { + final String label; + final Color backgroundColor; + final Color foregroundColor; + final VoidCallback onPressed; + + const ActionButton({ + super.key, + required this.label, + required this.backgroundColor, + required this.foregroundColor, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 120, + child: DefaultButton( + onPressed: onPressed, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + child: Text(label), + ), + ); + } +} diff --git a/lib/pages/spaces_management/tag_model/widgets/device_icon_widget.dart b/lib/pages/spaces_management/tag_model/widgets/device_icon_widget.dart new file mode 100644 index 00000000..4e8adbd9 --- /dev/null +++ b/lib/pages/spaces_management/tag_model/widgets/device_icon_widget.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DeviceIconWidget extends StatelessWidget { + final String? icon; + + const DeviceIconWidget({ + super.key, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 50, + width: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.textFieldGreyColor, + border: Border.all( + color: ColorsManager.neutralGray, + width: 2, + ), + ), + child: Center( + child: SvgPicture.asset( + icon ?? Assets.sensors, + width: 30, + height: 30, + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/tag_model/widgets/device_name_widget.dart b/lib/pages/spaces_management/tag_model/widgets/device_name_widget.dart new file mode 100644 index 00000000..11a102a8 --- /dev/null +++ b/lib/pages/spaces_management/tag_model/widgets/device_name_widget.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DeviceNameWidget extends StatelessWidget { + final String? name; + + const DeviceNameWidget({ + super.key, + required this.name, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 35, + child: Text( + name ?? '', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.blackColor), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + } +} diff --git a/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart b/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart new file mode 100644 index 00000000..7d103cdb --- /dev/null +++ b/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_name_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DeviceTypeTileWidget extends StatelessWidget { + final ProductModel product; + final List productCounts; + final bool isCreate; + + const DeviceTypeTileWidget( + {super.key, + required this.product, + required this.productCounts, + required this.isCreate}); + + @override + Widget build(BuildContext context) { + final selectedProduct = productCounts.firstWhere( + (p) => p.productId == product.uuid, + orElse: () => SelectedProduct( + productId: product.uuid, + count: 0, + productName: product.catName, + product: product), + ); + + return Card( + elevation: 2, + color: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DeviceIconWidget(icon: product.icon ?? Assets.doorSensor), + const SizedBox(height: 4), + DeviceNameWidget(name: product.name), + const SizedBox(height: 4), + CounterWidget( + isCreate: isCreate, + initialCount: selectedProduct.count, + onCountChanged: (newCount) { + context.read().add( + UpdateProductCountEvent( + productId: product.uuid, + count: newCount, + productName: product.catName, + product: product), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/tag_model/widgets/scrollable_grid_view_widget.dart b/lib/pages/spaces_management/tag_model/widgets/scrollable_grid_view_widget.dart new file mode 100644 index 00000000..d1775c66 --- /dev/null +++ b/lib/pages/spaces_management/tag_model/widgets/scrollable_grid_view_widget.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart'; + +class ScrollableGridViewWidget extends StatelessWidget { + final List? products; + final int crossAxisCount; + final List? initialProductCounts; + final bool isCreate; + + const ScrollableGridViewWidget({ + super.key, + required this.products, + required this.crossAxisCount, + this.initialProductCounts, + required this.isCreate + }); + + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + + return Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: BlocBuilder( + builder: (context, state) { + final productCounts = state is AddDeviceModelLoaded + ? state.selectedProducts + : []; + + return GridView.builder( + controller: scrollController, + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: .8, + ), + itemCount: products?.length ?? 0, + itemBuilder: (context, index) { + final product = products![index]; + final initialProductCount = _findInitialProductCount(product); + + return DeviceTypeTileWidget( + product: product, + isCreate: isCreate, + productCounts: initialProductCount != null + ? [...productCounts, initialProductCount] + : productCounts, + ); + }, + ); + }, + ), + ); + } + + SelectedProduct? _findInitialProductCount(ProductModel product) { + // Check if the product exists in initialProductCounts + if (initialProductCounts == null) return null; + final matchingProduct = initialProductCounts!.firstWhere( + (selectedProduct) => selectedProduct.productId == product.uuid, + orElse: () => SelectedProduct( + productId: '', + count: 0, + productName: '', + product: null, + ), + ); + + // Check if the product was actually found + return matchingProduct.productId.isNotEmpty ? matchingProduct : null; + } +} diff --git a/lib/services/home_api.dart b/lib/services/home_api.dart index dfbaf4bf..c1e67add 100644 --- a/lib/services/home_api.dart +++ b/lib/services/home_api.dart @@ -12,4 +12,33 @@ class HomeApi { }); return response; } + + Future fetchTerms() async { + final response = await HTTPService().get( + path: ApiEndpoints.terms, + showServerMessage: true, + expectedResponseModel: (json) { + return json['data']; + }); + return response; + } + + Future fetchPolicy() async { + final response = await HTTPService().get( + path: ApiEndpoints.policy, + showServerMessage: true, + expectedResponseModel: (json) { + return json['data']; + }); + return response; + } + + Future confirmUserAgreements(uuid) async { + final response = await HTTPService().patch( + path: ApiEndpoints.userAgreements.replaceAll('{userUuid}', uuid!), + expectedResponseModel: (json) { + return json['data']; + }); + return response; + } } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index def94441..2a2d42ad 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -1,8 +1,10 @@ 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/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.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/temp_const.dart'; @@ -20,23 +22,26 @@ class CommunitySpaceManagementApi { .replaceAll('{projectId}', TempConst.projectId), queryParameters: {'page': page}, expectedResponseModel: (json) { - List jsonData = json['data']; - hasNext = json['hasNext'] ?? false; - int currentPage = json['page'] ?? 1; - List communityList = jsonData.map((jsonItem) { - return CommunityModel.fromJson(jsonItem); - }).toList(); - - allCommunities.addAll(communityList); - page = currentPage + 1; - return communityList; + try { + List jsonData = json['data'] ?? []; + hasNext = json['hasNext'] ?? false; + int currentPage = json['page'] ?? 1; + List communityList = jsonData.map((jsonItem) { + return CommunityModel.fromJson(jsonItem); + }).toList(); + allCommunities.addAll(communityList); + page = currentPage + 1; + return communityList; + } catch (_) { + hasNext = false; + return []; + } }, ); } return allCommunities; } catch (e) { - debugPrint('Error fetching communities: $e'); return []; } } @@ -159,16 +164,17 @@ class CommunitySpaceManagementApi { } } - Future createSpace({ - required String communityId, - required String name, - String? parentId, - String? direction, - bool isPrivate = false, - required Offset position, - String? icon, - required List products, - }) async { + Future createSpace( + {required String communityId, + required String name, + String? parentId, + String? direction, + bool isPrivate = false, + required Offset position, + String? spaceModelUuid, + String? icon, + List? tags, + List? subspaces}) async { try { final body = { 'spaceName': name, @@ -177,11 +183,17 @@ class CommunitySpaceManagementApi { 'y': position.dy, 'direction': direction, 'icon': icon, - 'products': products.map((product) => product.toJson()).toList(), }; if (parentId != null) { body['parentUuid'] = parentId; } + if (tags != null) { + body['tags'] = tags; + } + + if (spaceModelUuid != null) body['spaceModelUuid'] = spaceModelUuid; + + if (subspaces != null) body['subspaces'] = subspaces; final response = await HTTPService().post( path: ApiEndpoints.createSpace .replaceAll('{communityId}', communityId) @@ -207,7 +219,6 @@ class CommunitySpaceManagementApi { String? direction, bool isPrivate = false, required Offset position, - required List products, }) async { try { final body = { @@ -217,7 +228,6 @@ class CommunitySpaceManagementApi { 'y': position.dy, 'direction': direction, 'icon': icon, - 'products': products.map((product) => product.toJson()).toList(), }; if (parentId != null) { body['parentUuid'] = parentId; diff --git a/lib/services/space_model_mang_api.dart b/lib/services/space_model_mang_api.dart new file mode 100644 index 00000000..eb896432 --- /dev/null +++ b/lib/services/space_model_mang_api.dart @@ -0,0 +1,63 @@ +import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.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/temp_const.dart'; + +class SpaceModelManagementApi { + Future> listSpaceModels({int page = 1}) async { + final response = await HTTPService().get( + path: ApiEndpoints.listSpaceModels + .replaceAll('{projectId}', TempConst.projectId), + queryParameters: {'page': page}, + expectedResponseModel: (json) { + List jsonData = json['data']; + return jsonData.map((jsonItem) { + return SpaceTemplateModel.fromJson(jsonItem); + }).toList(); + }, + ); + return response; + } + + Future createSpaceModel( + CreateSpaceTemplateBodyModel spaceModel) async { + final response = await HTTPService().post( + path: ApiEndpoints.createSpaceModel + .replaceAll('{projectId}', TempConst.projectId), + showServerMessage: true, + body: spaceModel.toJson(), + expectedResponseModel: (json) { + return SpaceTemplateModel.fromJson(json['data']); + }, + ); + return response; + } + + + Future updateSpaceModel( + CreateSpaceTemplateBodyModel spaceModel, String spaceModelUuid) async { + final response = await HTTPService().put( + path: ApiEndpoints.updateSpaceModel + .replaceAll('{projectId}', TempConst.projectId).replaceAll('{spaceModelUuid}', spaceModelUuid), + body: spaceModel.toJson(), + expectedResponseModel: (json) { + return json['message']; + }, + ); + return response; + } + + Future getSpaceModel(String spaceModelUuid) async { + final response = await HTTPService().get( + path: ApiEndpoints.getSpaceModel + .replaceAll('{projectId}', TempConst.projectId) + .replaceAll('{spaceModelUuid}', spaceModelUuid), + showServerMessage: true, + expectedResponseModel: (json) { + return SpaceTemplateModel.fromJson(json['data']); + }, + ); + return response; + } +} diff --git a/lib/services/user_permission.dart b/lib/services/user_permission.dart new file mode 100644 index 00000000..527010d0 --- /dev/null +++ b/lib/services/user_permission.dart @@ -0,0 +1,219 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/rendering.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/edit_user_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/role_type_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/model/permission_option_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class UserPermissionApi { + static final HTTPService _httpService = HTTPService(); + + Future> fetchUsers() async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getUsers, + showServerMessage: true, + expectedResponseModel: (json) { + debugPrint('fetchUsers Response: $json'); + final List data = + json['data'] ?? []; // Default to an empty list if no data + return data.map((item) => RolesUserModel.fromJson(item)).toList(); + }, + ); + return response; + } catch (e, stackTrace) { + debugPrint('Error in fetchUsers: $e'); + rethrow; + } + } + + fetchRoles() async { + final response = await _httpService.get( + path: ApiEndpoints.roleTypes, + showServerMessage: true, + expectedResponseModel: (json) { + final List fetchedRoles = (json['data'] as List) + .map((item) => RoleTypeModel.fromJson(item)) + .toList(); + return fetchedRoles; + }, + ); + return response; + } + + Future> fetchPermission(roleUuid) async { + final response = await _httpService.get( + path: ApiEndpoints.permission.replaceAll("roleUuid", roleUuid), + showServerMessage: true, + expectedResponseModel: (json) { + return (json as List) + .map((data) => PermissionOption.fromJson(data)) + .toList(); + }, + ); + return response ?? []; + } + + Future sendInviteUser({ + String? firstName, + String? lastName, + String? email, + String? jobTitle, + String? phoneNumber, + String? roleUuid, + List? spaceUuids, + }) async { + try { + final body = { + "firstName": firstName, + "lastName": lastName, + "email": email, + "jobTitle": jobTitle != '' ? jobTitle : null, + "phoneNumber": phoneNumber != '' ? phoneNumber : null, + "roleUuid": roleUuid, + "projectUuid": "0e62577c-06fa-41b9-8a92-99a21fbaf51c", + "spaceUuids": spaceUuids, + }; + final response = await _httpService.post( + path: ApiEndpoints.inviteUser, + showServerMessage: true, + body: jsonEncode(body), + expectedResponseModel: (json) { + if (json['statusCode'] != 400) { + return json["success"]; + } else { + return false; + } + }, + ); + print('sendInviteUser=$body'); + + return response ?? []; + } on DioException catch (e) { + return false; + } catch (e) { + return false; + } + } + + Future checkEmail(String email) async { + try { + final response = await _httpService.post( + path: ApiEndpoints.checkEmail, + showServerMessage: true, + body: {"email": email}, + expectedResponseModel: (json) { + if (json['statusCode'] != 400) { + var message = json["message"]; + if (message is String) { + return message; + } else { + return 'Unexpected message format'; + } + } + return null; + }, + ); + return response ?? 'Unknown error occurred'; + } on DioException catch (e) { + final errorMessage = e.response?.data['error']; + return errorMessage; + } catch (e) { + return e.toString(); + } + } + + Future fetchUserById(userUuid) async { + final response = await _httpService.get( + path: ApiEndpoints.getUserById.replaceAll("{userUuid}", userUuid), + showServerMessage: true, + expectedResponseModel: (json) { + EditUserModel res = EditUserModel.fromJson(json['data']); + return res; + }, + ); + return response; + } + + Future editInviteUser({ + String? firstName, + String? userId, + String? lastName, + String? jobTitle, + String? phoneNumber, + String? roleUuid, + List? spaceUuids, + }) async { + try { + final body = { + "firstName": firstName, + "lastName": lastName, + "jobTitle": jobTitle != '' ? jobTitle : " ", + "phoneNumber": phoneNumber != '' ? phoneNumber : " ", + "roleUuid": roleUuid, + "projectUuid": "0e62577c-06fa-41b9-8a92-99a21fbaf51c", + "spaceUuids": spaceUuids, + }; + final response = await _httpService.put( + path: ApiEndpoints.editUser.replaceAll('{inviteUserUuid}', userId!), + body: jsonEncode(body), + expectedResponseModel: (json) { + if (json['statusCode'] != 400) { + return json["success"]; + } else { + return false; + } + }, + ); + return response; + } on DioException catch (e) { + return false; + } catch (e) { + return false; + } + } + + Future deleteUserById(userUuid) async { + try { + final response = await _httpService.delete( + path: ApiEndpoints.deleteUser.replaceAll("{inviteUserUuid}", userUuid), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success']; + }, + ); + return response; + } catch (e) { + return false; + } + } + + Future changeUserStatusById(userUuid, status) async { + try { + Map bodya = { + "disable": status, + "projectUuid": "0e62577c-06fa-41b9-8a92-99a21fbaf51c" + }; + print('changeUserStatusById==$bodya'); + print('changeUserStatusById==$userUuid'); + + final response = await _httpService.put( + path: ApiEndpoints.changeUserStatus + .replaceAll("{invitedUserUuid}", userUuid), + body: bodya, + expectedResponseModel: (json) { + print('changeUserStatusById==${json['success']}'); + return json['success']; + }, + ); + + return response; + } catch (e) { + return false; + print(e); + } + } +} diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 31329d8a..ccc450fe 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -3,6 +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/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; +import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_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/utils/constants/routes_const.dart'; @@ -32,7 +33,10 @@ class AppRoutes { ), GoRoute( path: RoutesConst.spacesManagementPage, - builder: (context, state) => const SpaceManagementPage()), + builder: (context, state) => const SpaceManagementPage()), + GoRoute( + path: RoutesConst.rolesAndPermissions, + builder: (context, state) => const RolesAndPermissionPage()), ]; } } diff --git a/lib/utils/asset_validator.dart b/lib/utils/asset_validator.dart new file mode 100644 index 00000000..add6a3f4 --- /dev/null +++ b/lib/utils/asset_validator.dart @@ -0,0 +1,15 @@ +import 'package:flutter/services.dart'; + +class AssetValidator { + static Future isValidAsset(String? assetPath) async { + if (assetPath == null || assetPath.isEmpty) { + return false; + } + try { + await rootBundle.load(assetPath); + return true; + } catch (_) { + return false; + } + } +} diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 95d0f214..301365ed 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -5,8 +5,11 @@ abstract class ColorsManager { static const Color switchOffColor = Color(0x7F8D99AE); static const Color primaryColor = Color(0xFF0030CB); //023DFE static const Color secondaryTextColor = Color(0xFF848484); - static Color primaryColorWithOpacity = const Color(0xFF023DFE).withOpacity(0.6); + static Color primaryColorWithOpacity = + const Color(0xFF023DFE).withOpacity(0.6); static const Color whiteColors = Colors.white; + static Color whiteColorsWithOpacity = Colors.white.withOpacity(0.6); + static const Color secondaryColor = Color(0xFF023DFE); static const Color onSecondaryColor = Color(0xFF023DFE); static Color shadowBlackColor = Colors.black.withOpacity(0.2); @@ -54,5 +57,18 @@ abstract class ColorsManager { static const Color warningRed = Color(0xFFFF6465); static const Color borderColor = Color(0xFFE5E5E5); static const Color CircleImageBackground = Color(0xFFF4F4F4); + static const Color softGray = Color(0xFFD5D5D5); + static const Color semiTransparentBlack = Color(0x19000000); + static const Color dataHeaderGrey = Color(0x33999999); + static const Color circleImageBackground = Color(0xFFF4F4F4); + static const Color circleRolesBackground = Color(0xFFF8F8F8); + static const Color activeGreen = Color(0xFF99FF93); + static const Color activeGreenText = Color(0xFF008905); + static const Color disabledPink = Color(0xFFFF9395); + static const Color disabledRedText = Color(0xFF890002); + static const Color invitedOrange = Color(0xFFFFE193); + static const Color invitedOrangeText = Color(0xFFFFBF00); + static const Color lightGrayBorderColor = Color(0xB2D5D5D5); + //background: #F8F8F8; + } -//background: #background: #5D5D5D; diff --git a/lib/utils/constants/action_enum.dart b/lib/utils/constants/action_enum.dart new file mode 100644 index 00000000..2cfe53de --- /dev/null +++ b/lib/utils/constants/action_enum.dart @@ -0,0 +1,31 @@ +enum Action { + update, + add, + delete, +} + +extension ActionExtension on Action { + String get value { + switch (this) { + case Action.update: + return 'update'; + case Action.add: + return 'add'; + case Action.delete: + return 'delete'; + } + } + + static Action fromValue(String value) { + switch (value) { + case 'update': + return Action.update; + case 'add': + return Action.add; + case 'delete': + return Action.delete; + default: + throw ArgumentError('Invalid action: $value'); + } + } +} diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index c5fc4506..80c3c830 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -1,6 +1,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; abstract class ApiEndpoints { + static const String projectUuid = "0e62577c-06fa-41b9-8a92-99a21fbaf51c"; static String baseUrl = dotenv.env['BASE_URL'] ?? ''; static const String signUp = '/authentication/user/signup'; static const String login = '/authentication/user/login'; @@ -72,7 +73,7 @@ abstract class ApiEndpoints { static const String getIconScene = '/scene/icon'; static const String createScene = '/scene/tap-to-run'; static const String createAutomation = '/automation'; - static const String getScenes = + static const String getUnitScenes = '/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/scenes'; static const String getAutomationDetails = '/automation/details/{automationId}'; static const String getScene = '/scene/tap-to-run/{sceneId}'; @@ -82,4 +83,24 @@ abstract class ApiEndpoints { static const String updateScene = '/scene/tap-to-run/{sceneId}'; static const String updateAutomation = '/automation/{automationId}'; + + //space model + static const String listSpaceModels = '/projects/{projectId}/space-models'; + static const String createSpaceModel = '/projects/{projectId}/space-models'; + static const String getSpaceModel = '/projects/{projectId}/space-models/{spaceModelUuid}'; + static const String updateSpaceModel = '/projects/{projectId}/space-models/{spaceModelUuid}'; + + static const String roleTypes = '/role/types'; + static const String permission = '/permission/{roleUuid}'; + static const String inviteUser = '/invite-user'; + + static const String checkEmail = '/invite-user/check-email'; + static const String getUsers = '/projects/${projectUuid}/user'; + static const String getUserById = '/projects/${projectUuid}/user/{userUuid}'; + static const String editUser = '/invite-user/{inviteUserUuid}'; + static const String deleteUser = '/invite-user/{inviteUserUuid}'; + static const String changeUserStatus = '/invite-user/{invitedUserUuid}/disable'; + static const String terms = '/terms'; + static const String policy = '/policy'; + static const String userAgreements = '/user/agreements/web/{userUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 958c2c1c..a9deb3c7 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -13,10 +13,12 @@ class Assets { static const String rightLine = "assets/images/right_line.png"; static const String google = "assets/images/google.svg"; static const String facebook = "assets/images/facebook.svg"; - static const String invisiblePassword = "assets/images/Password_invisible.svg"; + static const String invisiblePassword = + "assets/images/Password_invisible.svg"; static const String visiblePassword = "assets/images/password_visible.svg"; static const String accessIcon = "assets/images/access_icon.svg"; - static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; + static const String spaseManagementIcon = + "assets/images/spase_management_icon.svg"; static const String devicesIcon = "assets/images/devices_icon.svg"; static const String moveinIcon = "assets/images/movein_icon.svg"; static const String constructionIcon = "assets/images/construction_icon.svg"; @@ -29,13 +31,15 @@ class Assets { static const String emptyTable = "assets/images/empty_table.svg"; // General assets - static const String motionlessDetection = "assets/icons/motionless_detection.svg"; + static const String motionlessDetection = + "assets/icons/motionless_detection.svg"; static const String acHeating = "assets/icons/ac_heating.svg"; static const String acPowerOff = "assets/icons/ac_power_off.svg"; static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; static const String switchAlarmSound = "assets/icons/switch_alarm_sound.svg"; static const String resetOff = "assets/icons/reset_off.svg"; - static const String sensitivityOperationIcon = "assets/icons/sesitivity_operation_icon.svg"; + static const String sensitivityOperationIcon = + "assets/icons/sesitivity_operation_icon.svg"; static const String motionDetection = "assets/icons/motion_detection.svg"; static const String freezing = "assets/icons/freezing.svg"; static const String indicator = "assets/icons/indicator.svg"; @@ -56,7 +60,8 @@ class Assets { static const String celsiusDegrees = "assets/icons/celsius_degrees.svg"; static const String masterState = "assets/icons/master_state.svg"; static const String acPower = "assets/icons/ac_power.svg"; - static const String farDetectionFunction = "assets/icons/far_detection_function.svg"; + static const String farDetectionFunction = + "assets/icons/far_detection_function.svg"; static const String nobodyTime = "assets/icons/nobody_time.svg"; // Automation functions @@ -64,33 +69,47 @@ class Assets { "assets/icons/automation_functions/temp_password_unlock.svg"; static const String doorlockNormalOpen = "assets/icons/automation_functions/doorlock_normal_open.svg"; - static const String doorbell = "assets/icons/automation_functions/doorbell.svg"; + static const String doorbell = + "assets/icons/automation_functions/doorbell.svg"; static const String remoteUnlockViaApp = "assets/icons/automation_functions/remote_unlock_via_app.svg"; - static const String doubleLock = "assets/icons/automation_functions/double_lock.svg"; - static const String selfTestResult = "assets/icons/automation_functions/self_test_result.svg"; - static const String lockAlarm = "assets/icons/automation_functions/lock_alarm.svg"; - static const String presenceState = "assets/icons/automation_functions/presence_state.svg"; - static const String currentTemp = "assets/icons/automation_functions/current_temp.svg"; - static const String presence = "assets/icons/automation_functions/presence.svg"; + static const String doubleLock = + "assets/icons/automation_functions/double_lock.svg"; + static const String selfTestResult = + "assets/icons/automation_functions/self_test_result.svg"; + static const String lockAlarm = + "assets/icons/automation_functions/lock_alarm.svg"; + static const String presenceState = + "assets/icons/automation_functions/presence_state.svg"; + static const String currentTemp = + "assets/icons/automation_functions/current_temp.svg"; + static const String presence = + "assets/icons/automation_functions/presence.svg"; static const String residualElectricity = "assets/icons/automation_functions/residual_electricity.svg"; - static const String hijackAlarm = "assets/icons/automation_functions/hijack_alarm.svg"; - static const String passwordUnlock = "assets/icons/automation_functions/password_unlock.svg"; + static const String hijackAlarm = + "assets/icons/automation_functions/hijack_alarm.svg"; + static const String passwordUnlock = + "assets/icons/automation_functions/password_unlock.svg"; static const String remoteUnlockRequest = "assets/icons/automation_functions/remote_unlock_req.svg"; - static const String cardUnlock = "assets/icons/automation_functions/card_unlock.svg"; + static const String cardUnlock = + "assets/icons/automation_functions/card_unlock.svg"; static const String motion = "assets/icons/automation_functions/motion.svg"; static const String fingerprintUnlock = "assets/icons/automation_functions/fingerprint_unlock.svg"; // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; - static const String sensorPresenceIcon = "assets/icons/sensor_presence_ic.svg"; + static const String sensorPresenceIcon = + "assets/icons/sensor_presence_ic.svg"; static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; - static const String illuminanceRecordIcon = "assets/icons/illuminance_record_ic.svg"; - static const String presenceRecordIcon = "assets/icons/presence_record_ic.svg"; - static const String helpDescriptionIcon = "assets/icons/help_description_ic.svg"; + static const String illuminanceRecordIcon = + "assets/icons/illuminance_record_ic.svg"; + static const String presenceRecordIcon = + "assets/icons/presence_record_ic.svg"; + static const String helpDescriptionIcon = + "assets/icons/help_description_ic.svg"; static const String lightPulp = "assets/icons/light_pulb.svg"; static const String acDevice = "assets/icons/ac_device.svg"; @@ -140,10 +159,12 @@ class Assets { static const String unit = 'assets/icons/unit_icon.svg'; static const String villa = 'assets/icons/villa_icon.svg'; static const String iconEdit = 'assets/icons/icon_edit_icon.svg'; - static const String textFieldSearch = 'assets/icons/textfield_search_icon.svg'; + static const String textFieldSearch = + 'assets/icons/textfield_search_icon.svg'; static const String roundedAddIcon = 'assets/icons/rounded_add_icon.svg'; static const String addIcon = 'assets/icons/add_icon.svg'; - static const String smartThermostatIcon = 'assets/icons/smart_thermostat_icon.svg'; + static const String smartThermostatIcon = + 'assets/icons/smart_thermostat_icon.svg'; static const String smartLightIcon = 'assets/icons/smart_light_icon.svg'; static const String presenceSensor = 'assets/icons/presence_sensor.svg'; static const String Gang3SwitchIcon = 'assets/icons/3_Gang_switch_icon.svg'; @@ -191,7 +212,8 @@ class Assets { //assets/icons/water_leak_normal.svg static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; //assets/icons/water_leak_detected.svg - static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; + static const String waterLeakDetected = + 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -256,40 +278,64 @@ class Assets { static const String delay = 'assets/icons/routine/delay.svg'; // Assets for functions_icons - static const String assetsSensitivityFunction = "assets/icons/functions_icons/sensitivity.svg"; + static const String assetsSensitivityFunction = + "assets/icons/functions_icons/sensitivity.svg"; static const String assetsSensitivityOperationIcon = "assets/icons/functions_icons/sesitivity_operation_icon.svg"; - static const String assetsAcPower = "assets/icons/functions_icons/ac_power.svg"; - static const String assetsAcPowerOFF = "assets/icons/functions_icons/ac_power_off.svg"; - static const String assetsChildLock = "assets/icons/functions_icons/child_lock.svg"; - static const String assetsFreezing = "assets/icons/functions_icons/freezing.svg"; - static const String assetsFanSpeed = "assets/icons/functions_icons/fan_speed.svg"; - static const String assetsAcCooling = "assets/icons/functions_icons/ac_cooling.svg"; - static const String assetsAcHeating = "assets/icons/functions_icons/ac_heating.svg"; - static const String assetsCelsiusDegrees = "assets/icons/functions_icons/celsius_degrees.svg"; - static const String assetsTempreture = "assets/icons/functions_icons/tempreture.svg"; - static const String assetsAcFanLow = "assets/icons/functions_icons/ac_fan_low.svg"; - static const String assetsAcFanMiddle = "assets/icons/functions_icons/ac_fan_middle.svg"; - static const String assetsAcFanHigh = "assets/icons/functions_icons/ac_fan_high.svg"; - static const String assetsAcFanAuto = "assets/icons/functions_icons/ac_fan_auto.svg"; - static const String assetsSceneChildLock = "assets/icons/functions_icons/scene_child_lock.svg"; + static const String assetsAcPower = + "assets/icons/functions_icons/ac_power.svg"; + static const String assetsAcPowerOFF = + "assets/icons/functions_icons/ac_power_off.svg"; + static const String assetsChildLock = + "assets/icons/functions_icons/child_lock.svg"; + static const String assetsFreezing = + "assets/icons/functions_icons/freezing.svg"; + static const String assetsFanSpeed = + "assets/icons/functions_icons/fan_speed.svg"; + static const String assetsAcCooling = + "assets/icons/functions_icons/ac_cooling.svg"; + static const String assetsAcHeating = + "assets/icons/functions_icons/ac_heating.svg"; + static const String assetsCelsiusDegrees = + "assets/icons/functions_icons/celsius_degrees.svg"; + static const String assetsTempreture = + "assets/icons/functions_icons/tempreture.svg"; + static const String assetsAcFanLow = + "assets/icons/functions_icons/ac_fan_low.svg"; + static const String assetsAcFanMiddle = + "assets/icons/functions_icons/ac_fan_middle.svg"; + static const String assetsAcFanHigh = + "assets/icons/functions_icons/ac_fan_high.svg"; + static const String assetsAcFanAuto = + "assets/icons/functions_icons/ac_fan_auto.svg"; + static const String assetsSceneChildLock = + "assets/icons/functions_icons/scene_child_lock.svg"; static const String assetsSceneChildUnlock = "assets/icons/functions_icons/scene_child_unlock.svg"; - static const String assetsSceneRefresh = "assets/icons/functions_icons/scene_refresh.svg"; - static const String assetsLightCountdown = "assets/icons/functions_icons/light_countdown.svg"; - static const String assetsFarDetection = "assets/icons/functions_icons/far_detection.svg"; + static const String assetsSceneRefresh = + "assets/icons/functions_icons/scene_refresh.svg"; + static const String assetsLightCountdown = + "assets/icons/functions_icons/light_countdown.svg"; + static const String assetsFarDetection = + "assets/icons/functions_icons/far_detection.svg"; static const String assetsFarDetectionFunction = "assets/icons/functions_icons/far_detection_function.svg"; - static const String assetsIndicator = "assets/icons/functions_icons/indicator.svg"; - static const String assetsMotionDetection = "assets/icons/functions_icons/motion_detection.svg"; + static const String assetsIndicator = + "assets/icons/functions_icons/indicator.svg"; + static const String assetsMotionDetection = + "assets/icons/functions_icons/motion_detection.svg"; static const String assetsMotionlessDetection = "assets/icons/functions_icons/motionless_detection.svg"; - static const String assetsNobodyTime = "assets/icons/functions_icons/nobody_time.svg"; - static const String assetsFactoryReset = "assets/icons/functions_icons/factory_reset.svg"; - static const String assetsMasterState = "assets/icons/functions_icons/master_state.svg"; + static const String assetsNobodyTime = + "assets/icons/functions_icons/nobody_time.svg"; + static const String assetsFactoryReset = + "assets/icons/functions_icons/factory_reset.svg"; + static const String assetsMasterState = + "assets/icons/functions_icons/master_state.svg"; static const String assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; - static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; + static const String assetsResetOff = + "assets/icons/functions_icons/reset_off.svg"; // Assets for automation_functions static const String assetsCardUnlock = @@ -320,11 +366,37 @@ class Assets { "assets/icons/functions_icons/automation_functions/self_test_result.svg"; static const String assetsPresence = "assets/icons/functions_icons/automation_functions/presence.svg"; - static const String assetsMotion = "assets/icons/functions_icons/automation_functions/motion.svg"; + static const String assetsMotion = + "assets/icons/functions_icons/automation_functions/motion.svg"; static const String assetsCurrentTemp = "assets/icons/functions_icons/automation_functions/current_temp.svg"; static const String assetsPresenceState = "assets/icons/functions_icons/automation_functions/presence_state.svg"; //assets/icons/routine/automation.svg static const String automation = 'assets/icons/routine/automation.svg'; + static const String searchIconUser = 'assets/icons/search_icon_user.svg'; + static const String searchIcoUser = 'assets/icons/search_icon_user.svg'; + static const String activeUser = 'assets/icons/active_user.svg'; + static const String deActiveUser = 'assets/icons/deactive_user.svg'; + static const String invitedIcon = 'assets/icons/invited_icon.svg'; + static const String rectangleCheckBox = + 'assets/icons/rectangle_check_box.png'; + static const String CheckBoxChecked = 'assets/icons/box_checked.png'; + static const String emptyBox = 'assets/icons/empty_box.png'; + static const String completeProcessIcon = + 'assets/icons/compleate_process_icon.svg'; + static const String currentProcessIcon = + 'assets/icons/current_process_icon.svg'; + static const String uncomplete_ProcessIcon = + 'assets/icons/uncompleate_process_icon.svg'; + static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg'; + static const String arrowForward = 'assets/icons/arrow_forward.svg'; + static const String arrowDown = 'assets/icons/arrow_down.svg'; + + static const String userManagement = 'assets/icons/user_management.svg'; + static const String filterTableIcon = 'assets/icons/filter_table_icon.svg'; + static const String ZtoAIcon = 'assets/icons/ztoa_icon.png'; + static const String AtoZIcon = 'assets/icons/atoz_icon.png'; + static const String link = 'assets/icons/link.svg'; } +//user_management.svg diff --git a/lib/utils/constants/routes_const.dart b/lib/utils/constants/routes_const.dart index 094787d4..8a65e9ae 100644 --- a/lib/utils/constants/routes_const.dart +++ b/lib/utils/constants/routes_const.dart @@ -5,4 +5,5 @@ class RoutesConst { static const String accessManagementPage = '/access-management-page'; static const String deviceManagementPage = '/device-management-page'; static const String spacesManagementPage = '/spaces_management-page'; + static const String rolesAndPermissions = '/roles_and_Permissions-page'; } diff --git a/lib/utils/style.dart b/lib/utils/style.dart index a80c68d6..b5ea59ee 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -2,51 +2,59 @@ import 'package:flutter/material.dart'; import 'color_manager.dart'; -InputDecoration? textBoxDecoration({bool suffixIcon = false}) => InputDecoration( +InputDecoration? textBoxDecoration( + {bool suffixIcon = false, double radios = 8}) => + InputDecoration( focusColor: ColorsManager.grayColor, suffixIcon: suffixIcon ? const Icon(Icons.search) : null, hintText: 'Search', filled: true, // Enable background filling fillColor: const Color(0xffF5F6F7), // Set the background color border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), // Add border radius + borderRadius: BorderRadius.circular(radios), // Add border radius borderSide: BorderSide.none, // Remove the underline ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), // Add border radius + borderRadius: BorderRadius.circular(radios), // Add border radius borderSide: BorderSide.none, // Remove the underline ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), // Add border radius + borderRadius: BorderRadius.circular(radios), // Add border radius borderSide: BorderSide.none, // Remove the underline ), errorBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.red, width: 2), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(radios), ), focusedErrorBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.red, width: 2), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(radios), ), ); -BoxDecoration containerDecoration = BoxDecoration(boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(0, 5), // changes position of shadow - ), -], color: ColorsManager.boxColor, borderRadius: const BorderRadius.all(Radius.circular(10))); +BoxDecoration containerDecoration = BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow + ), + ], + color: ColorsManager.boxColor, + borderRadius: const BorderRadius.all(Radius.circular(10))); -BoxDecoration containerWhiteDecoration = BoxDecoration(boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(0, 5), // changes position of shadow - ), -], color: ColorsManager.whiteColors, borderRadius: const BorderRadius.all(Radius.circular(15))); +BoxDecoration containerWhiteDecoration = BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow + ), + ], + color: ColorsManager.whiteColors, + borderRadius: const BorderRadius.all(Radius.circular(15))); BoxDecoration subSectionContainerDecoration = BoxDecoration( color: ColorsManager.whiteColors, @@ -59,3 +67,30 @@ BoxDecoration subSectionContainerDecoration = BoxDecoration( ), ], ); + +InputDecoration inputTextFormDeco({hintText}) => InputDecoration( + hintText: hintText, + border: const OutlineInputBorder( + + borderSide: BorderSide( + width: 1, + color: ColorsManager.textGray, // Border color for unfocused state + ), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + width: 2, + color: ColorsManager.textGray, // Border color when focused + ), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + width: 1, + color: ColorsManager + .textGray // Border color for enabled (but unfocused) state + ), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ); diff --git a/lib/utils/user_drop_down_menu.dart b/lib/utils/user_drop_down_menu.dart index cee9bbe9..2d740e27 100644 --- a/lib/utils/user_drop_down_menu.dart +++ b/lib/utils/user_drop_down_menu.dart @@ -53,7 +53,8 @@ class _UserDropdownMenuState extends State { } Future _showPopupMenu(BuildContext context) async { - final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; final RelativeRect position = RelativeRect.fromRect( Rect.fromLTRB( overlay.size.width, @@ -85,29 +86,31 @@ class _UserDropdownMenuState extends State { // ), // ), // ), - // PopupMenuItem( - // onTap: () {}, - // child: ListTile( - // leading: SvgPicture.asset(Assets.settings), - // title: Text( - // "Settings", - // style: context.textTheme.bodyMedium, - // ), - // ), - // ), + PopupMenuItem( + onTap: () { + context.go(RoutesConst.rolesAndPermissions); + }, + child: ListTile( + leading: SvgPicture.asset(Assets.userManagement), + title: Text( + "User Management", + style: context.textTheme.bodyMedium, + ), + ), + ), PopupMenuItem( onTap: () { showDialog( context: context, builder: (BuildContext context) { - final size = MediaQuery.of(context).size; return AlertDialog( alignment: Alignment.center, content: SizedBox( height: 200, width: 400, child: Padding( - padding: const EdgeInsets.only(top: 24, left: 24, right: 24), + padding: + const EdgeInsets.only(top: 24, left: 24, right: 24), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -120,7 +123,10 @@ class _UserDropdownMenuState extends State { padding: const EdgeInsets.only(top: 16), child: Text( 'Log out of your Syncrow account', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( fontSize: 14, fontWeight: FontWeight.w400, color: Colors.black, @@ -151,11 +157,15 @@ class _UserDropdownMenuState extends State { ), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( '${widget.user?.firstName ?? ''} ${widget.user?.lastName}', - style: Theme.of(context).textTheme.titleMedium!.copyWith( + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20, @@ -163,7 +173,10 @@ class _UserDropdownMenuState extends State { ), Text( ' ${widget.user?.email}', - style: Theme.of(context).textTheme.bodySmall!.copyWith( + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( color: Colors.black, ), ), @@ -189,7 +202,10 @@ class _UserDropdownMenuState extends State { elevation: 1, child: Text( 'Cancel', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( fontSize: 12, color: Colors.black, ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d0e7f797..38dd0bc6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b29e9ba0..65240e99 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 37af1fe0..51aae316 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,11 @@ import Foundation import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 00000000..0639648b --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,36 @@ +PODS: + - flutter_secure_storage_macos (6.1.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + +SPEC CHECKSUMS: + flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index e1af4c91..eec16af6 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 108157F896CD9F637B06D7C0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DAF1C60594A51D692304366 /* Pods_Runner.framework */; }; + 2D0F1F294F673EF0DB5E4CA1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E148CBDFFE42BF88E8C34DE0 /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; @@ -60,11 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 24D7BEF98D33245EFB9F6A1B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* syncrow_web.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "syncrow_web.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* syncrow_web.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = syncrow_web.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5DAF1C60594A51D692304366 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 81F2F315AC5109F6F5D27BE6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 96C46007EE0A4E9E1D6D74CE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A604E311B663FBF4B7C54DC5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + AB949539E0D0A8E2BDAB9ADF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + E148CBDFFE42BF88E8C34DE0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F244F079A053D959E1C5C362 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2D0F1F294F673EF0DB5E4CA1 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 108157F896CD9F637B06D7C0 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 75DCDFECC7757C5159E8F0C5 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 75DCDFECC7757C5159E8F0C5 /* Pods */ = { + isa = PBXGroup; + children = ( + 24D7BEF98D33245EFB9F6A1B /* Pods-Runner.debug.xcconfig */, + F244F079A053D959E1C5C362 /* Pods-Runner.release.xcconfig */, + AB949539E0D0A8E2BDAB9ADF /* Pods-Runner.profile.xcconfig */, + 96C46007EE0A4E9E1D6D74CE /* Pods-RunnerTests.debug.xcconfig */, + A604E311B663FBF4B7C54DC5 /* Pods-RunnerTests.release.xcconfig */, + 81F2F315AC5109F6F5D27BE6 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 5DAF1C60594A51D692304366 /* Pods_Runner.framework */, + E148CBDFFE42BF88E8C34DE0 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + A1935203066F42991FF0ED43 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 8ECFD939A4D371A145DBA191 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 92D754792F50A5D35F6D5AEE /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -329,6 +361,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 8ECFD939A4D371A145DBA191 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 92D754792F50A5D35F6D5AEE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A1935203066F42991FF0ED43 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 96C46007EE0A4E9E1D6D74CE /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A604E311B663FBF4B7C54DC5 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 81F2F315AC5109F6F5D27BE6 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/pubspec.lock b/pubspec.lock index 192106d7..d1c39c65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -304,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + intl_phone_field: + dependency: "direct main" + description: + name: intl_phone_field + sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf" + url: "https://pub.dev" + source: hosted + version: "3.2.0" js: dependency: transitive description: @@ -316,18 +324,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -364,18 +372,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" nested: dependency: transitive description: @@ -384,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + number_pagination: + dependency: "direct main" + description: + name: number_pagination + sha256: "75d3a28616196e7c8df431d0fb7c48e811e462155f4cf3b5b4167b3408421327" + url: "https://pub.dev" + source: hosted + version: "1.1.6" path: dependency: transitive description: @@ -593,10 +609,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" time_picker_spinner: dependency: "direct main" description: @@ -657,10 +673,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ffff62ac..f4108d5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,10 @@ dependencies: fl_chart: ^0.69.0 uuid: ^4.4.2 time_picker_spinner: ^1.0.0 + intl_phone_field: ^3.2.0 + number_pagination: ^1.1.6 + url_launcher: ^6.2.5 + flutter_html: ^3.0.0-beta.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0c507538..2048c455 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4fc759c4..de626cc8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST