Merged with dev

This commit is contained in:
Abdullah Alassaf
2025-01-23 01:18:30 +03:00
173 changed files with 13556 additions and 564 deletions
+138
View File
@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DialogDropdown extends StatefulWidget {
final List<String> items;
final ValueChanged<String> 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<DialogDropdown> {
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),
],
),
),
);
}
}
+160
View File
@@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DialogTextfieldDropdown extends StatefulWidget {
final List<String> items;
final ValueChanged<String> 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<DialogTextfieldDropdown> {
bool _isOpen = false;
late OverlayEntry _overlayEntry;
final TextEditingController _controller = TextEditingController();
late List<String> _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),
],
),
),
);
}
}
+38
View File
@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return UserRole(
uuid: json['uuid'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
type: json['type'],
);
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ Future<void> showCustomDialog({
double? iconWidth,
VoidCallback? onOkPressed,
bool barrierDismissible = false,
required List<Widget> actions,
List<Widget>? actions,
}) {
return showDialog(
context: context,
@@ -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 {
+40 -1
View File
@@ -18,10 +18,15 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
// List<Node> sourcesList = [];
// List<Node> destinationsList = [];
UserModel? user;
String terms = '';
String policy = '';
HomeBloc() : super((HomeInitial())) {
// on<CreateNewNode>(_createNode);
on<FetchUserInfo>(_fetchUserInfo);
on<FetchTermEvent>(_fetchTerms);
on<FetchPolicyEvent>(_fetchPolicy);
on<ConfirmUserAgreementEvent>(_confirmUserAgreement);
}
// void _createNode(CreateNewNode event, Emitter<HomeState> emit) async {
@@ -42,14 +47,48 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> 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<HomeState> emit) async {
try {
emit(LoadingHome());
terms = await HomeApi().fetchTerms();
add(FetchPolicyEvent());
} catch (e) {
return;
}
}
Future _fetchPolicy(FetchPolicyEvent event, Emitter<HomeState> emit) async {
try {
emit(LoadingHome());
policy = await HomeApi().fetchPolicy();
} catch (e) {
return;
}
}
Future _confirmUserAgreement(
ConfirmUserAgreementEvent event, Emitter<HomeState> 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 =
+6
View File
@@ -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 {}
+6
View File
@@ -8,8 +8,14 @@ abstract class HomeState extends Equatable {
List<Object> 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);
@@ -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<AgreementAndPrivacyDialog> {
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(),
],
),
),
],
),
);
}
}
+28 -2
View File
@@ -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<HomeBloc>(context);
return PopScope(
canPop: false,
onPopInvoked: (didPop) => false,
child: BlocConsumer<HomeBloc, HomeState>(
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<HomeBloc>(context);
return WebScaffold(
enableMenuSidebar: false,
appBarTitle: Row(
@@ -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<RolesPermissionEvent, RolesPermissionState> {
RolesPermissionBloc() : super(RolesInitial()) {
on<GetRoles>(_getRoles);
on<ChangeTapSelected>(changeTapSelected);
}
List<RoleModel> roleModel = [];
FutureOr<void> _getRoles(
GetRoles event, Emitter<RolesPermissionState> 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<RolesPermissionState> emit) {
try {
emit(RolesLoadingState());
tapSelect = event.selected;
emit(ChangeTapStatus(select: !tapSelect));
} catch (e) {
emit(ErrorState(e.toString()));
}
}
}
@@ -0,0 +1,41 @@
import 'package:equatable/equatable.dart';
sealed class RolesPermissionEvent extends Equatable {
const RolesPermissionEvent();
}
class GetRoles extends RolesPermissionEvent {
const GetRoles();
@override
List<Object?> get props => [];
}
class GetBatchStatus extends RolesPermissionEvent {
final List<String> uuids;
const GetBatchStatus(this.uuids);
@override
List<Object?> get props => [uuids];
}
class GetDeviceRecords extends RolesPermissionEvent {
final String uuid;
const GetDeviceRecords(this.uuid);
@override
List<Object?> get props => [uuid];
}
class GetDeviceAutomationRecords extends RolesPermissionEvent {
final String uuid;
const GetDeviceAutomationRecords(this.uuid);
@override
List<Object?> get props => [uuid];
}
class ChangeTapSelected extends RolesPermissionEvent {
final bool selected;
const ChangeTapSelected(this.selected);
@override
List<Object?> get props => [selected];
}
@@ -0,0 +1,79 @@
import 'package:equatable/equatable.dart';
sealed class RolesPermissionState extends Equatable {
const RolesPermissionState();
}
final class RolesInitial extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class RolesLoadingState extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class UsersLoadingState extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class RolesLoadedState extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class UsersLoadedState extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class ErrorState extends RolesPermissionState {
final String message;
const ErrorState(this.message);
@override
List<Object> get props => [message];
}
/// report state
final class SosReportLoadingState extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class RolesErrorState extends RolesPermissionState {
final String message;
const RolesErrorState(this.message);
@override
List<Object> get props => [message];
}
/// automation reports
final class SosAutomationReportLoadingState extends RolesPermissionState {
@override
List<Object> get props => [];
}
final class SosAutomationReportErrorState extends RolesPermissionState {
final String message;
const SosAutomationReportErrorState(this.message);
@override
List<Object> get props => [message];
}
final class ChangeTapStatus extends RolesPermissionState {
final bool select;
const ChangeTapStatus({required this.select});
@override
List<Object> get props => [select];
}
@@ -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<String, dynamic> json) {
// return UserSpaceModel(
// uuid: json['uuid'],
// createdAt: DateTime.parse(json['createdAt']),
// updatedAt: DateTime.parse(json['updatedAt']),
// );
// }
// Map<String, dynamic> 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<UserSpaceModel> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return UserProjectResponse(
statusCode: json['statusCode'] as int,
message: json['message'] as String,
data: EditUserModel.fromJson(json['data'] as Map<String, dynamic>),
success: json['success'] as bool,
);
}
/// Convert the [UserProjectResponse] to JSON
Map<String, dynamic> 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<UserSpaceModel> 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<String, dynamic> 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<dynamic>)
.map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
/// Convert the [UserData] to JSON
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt,
'updatedAt': updatedAt,
'spaceTuyaUuid': spaceTuyaUuid,
'spaceName': spaceName,
'invitationCode': invitationCode,
'disabled': disabled,
'x': x,
'y': y,
'icon': icon,
};
}
}
@@ -0,0 +1,6 @@
class RoleModel {
String? roleId;
String? roleName;
String? roleImage;
RoleModel({this.roleId, this.roleName, this.roleImage});
}
@@ -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<String, dynamic> json) {
return RoleTypeModel(
uuid: json['uuid'],
createdAt: json['createdAt'],
updatedAt: json['updatedAt'],
type: json['type'].toString().toLowerCase().replaceAll("_", " "),
);
}
}
@@ -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<String, dynamic> 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'],
);
}
}
@@ -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<UsersEvent, UsersState> {
UsersBloc() : super(UsersInitial()) {
on<CheckStepStatus>(isCompleteBasicsFun);
on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces);
on<SearchAnode>(searchTreeNode);
on<CheckSpacesStepStatus>(isCompleteSpacesFun);
on<RoleEvent>(_getRolePermission);
on<PermissionEvent>(_getPermissions);
on<SearchPermission>(searchRolePermission);
on<SendInviteUsers>(_sendInvitUser);
on<ValidateBasicsStep>(_validateBasicsStep);
on<CheckRoleStepStatus>(isCompleteRoleFun);
on<CheckEmailEvent>(checkEmail);
on<GetUserByIdEvent>(getUserById);
on<ToggleNodeExpansion>(_onToggleNodeExpansion);
on<ToggleNodeCheck>(_onToggleNodeCheck);
on<EditInviteUsers>(_editInviteUser);
}
void _validateBasicsStep(ValidateBasicsStep event, Emitter<UsersState> emit) {
if (formKey.currentState?.validate() ?? false) {
emit(const BasicsStepValidState());
} else {
emit(const BasicsStepInvalidState());
}
}
String roleSelected = '';
final formKey = GlobalKey<FormState>();
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<UsersState> emit) {
emit(UsersLoadingState());
List<String> selectedIds = getSelectedIds(updatedCommunities);
isCompleteSpaces = selectedIds.isNotEmpty;
emit(ChangeStatusSteps());
}
void isCompleteRoleFun(CheckRoleStepStatus event, Emitter<UsersState> emit) {
emit(UsersLoadingState());
isCompleteRolePermissions = roleSelected != '';
emit(ChangeStatusSteps());
}
Future<List<SpaceModel>> _fetchSpacesForCommunity(
String communityUuid) async {
return await CommunitySpaceManagementApi().getSpaceHierarchy(communityUuid);
}
List<TreeNode> updatedCommunities = [];
List<TreeNode> spacesNodes = [];
_onLoadCommunityAndSpaces(
LoadCommunityAndSpacesEvent event, Emitter<UsersState> emit) async {
try {
emit(UsersLoadingState());
List<CommunityModel> communities =
await CommunitySpaceManagementApi().fetchCommunities();
updatedCommunities = await Future.wait(
communities.map((community) async {
List<SpaceModel> 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<TreeNode> _buildTreeNodes(List<SpaceModel> spaces) {
return spaces.map((space) {
List<TreeNode> 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<UsersState> emit) {
emit(UsersLoadingState());
if (event.searchTerm!.isEmpty) {
_clearHighlights(updatedCommunities);
} else {
_searchAndHighlightNodes(updatedCommunities, event.searchTerm!);
}
emit(ChangeStatusSteps());
}
void _clearHighlights(List<TreeNode> nodes) {
for (var node in nodes) {
node.isHighlighted = false;
if (node.children.isNotEmpty) {
_clearHighlights(node.children);
}
}
}
bool _searchAndHighlightNodes(List<TreeNode> 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<String> selectedIds = [];
List<String> getSelectedIds(List<TreeNode> nodes) {
List<String> 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<RoleTypeModel> roles = [];
List<PermissionOption> permissions = [];
_getRolePermission(RoleEvent event, Emitter<UsersState> 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<UsersState> 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<PermissionOption> 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<UsersState> emit) async {
try {
emit(UsersLoadingState());
List<String> 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: <Widget>[
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<UsersState> emit) async {
try {
emit(UsersLoadingState());
List<String> 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: <Widget>[
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<UsersState> emit) {
emit(UsersLoadingState());
if (event.searchTerm!.isEmpty) {
_clearHighlightsRolePermission(permissions);
} else {
_searchRolePermission(permissions, event.searchTerm!);
}
emit(ChangeStatusSteps());
}
String checkEmailValid = '';
Future<void> checkEmail(
CheckEmailEvent event, Emitter<UsersState> emit) async {
emit(UsersLoadingState());
String? res = await UserPermissionApi().checkEmail(
emailController.text,
);
checkEmailValid = res!;
emit(ChangeStatusSteps());
}
bool isCompleteBasicsFun(CheckStepStatus event, Emitter<UsersState> 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<PermissionOption> 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<void> getUserById(
GetUserByIdEvent event,
Emitter<UsersState> 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<TreeNode> nodes, List<String> 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<UsersState> emit,
) {
emit(UsersLoadingState());
event.node.isExpanded = !event.node.isExpanded;
emit(ChangeStatusSteps());
}
void _onToggleNodeCheck(
ToggleNodeCheck event,
Emitter<UsersState> 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<TreeNode> 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;
}
}
@@ -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<Object?> get props => [context];
}
class EditInviteUsers extends UsersEvent {
final BuildContext context;
final String userId;
const EditInviteUsers({required this.context, required this.userId});
@override
List<Object?> get props => [context, userId];
}
class CheckSpacesStepStatus extends UsersEvent {
const CheckSpacesStepStatus();
@override
List<Object?> get props => [];
}
class CheckRoleStepStatus extends UsersEvent {
const CheckRoleStepStatus();
@override
List<Object?> get props => [];
}
class LoadCommunityAndSpacesEvent extends UsersEvent {
const LoadCommunityAndSpacesEvent();
@override
List<Object?> get props => [];
}
class RoleEvent extends UsersEvent {
const RoleEvent();
@override
List<Object?> get props => [];
}
class PermissionEvent extends UsersEvent {
final String? roleUuid;
const PermissionEvent({this.roleUuid = ""});
@override
List<Object?> get props => [roleUuid];
}
class GetBatchStatus extends UsersEvent {
final List<String> uuids;
const GetBatchStatus(this.uuids);
@override
List<Object?> 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<Object?> get props => [steps];
}
class SearchAnode extends UsersEvent {
final List<TreeNode>? nodes;
final String? searchTerm;
const SearchAnode({this.nodes, this.searchTerm});
@override
List<Object?> get props => [nodes, searchTerm];
}
class SearchPermission extends UsersEvent {
final List<PermissionOption>? nodes;
final String? searchTerm;
const SearchPermission({this.nodes, this.searchTerm});
@override
List<Object?> get props => [nodes, searchTerm];
}
class SelectedId extends UsersEvent {
final List<TreeNode>? nodes;
const SelectedId({
this.nodes,
});
@override
List<Object?> get props => [nodes];
}
class ValidateBasicsStep extends UsersEvent {
const ValidateBasicsStep();
@override
List<Object?> get props => [];
}
class CheckEmailEvent extends UsersEvent {
const CheckEmailEvent();
@override
List<Object?> get props => [];
}
class GetUserByIdEvent extends UsersEvent {
final String? uuid;
const GetUserByIdEvent({this.uuid});
@override
List<Object?> get props => [uuid];
}
class ToggleNodeExpansion extends UsersEvent {
final TreeNode node;
const ToggleNodeExpansion({required this.node});
@override
List<Object?> get props => [node];
}
class UpdateNodeCheckStatus extends UsersEvent {
final TreeNode node;
const UpdateNodeCheckStatus({required this.node});
@override
List<Object?> get props => [node];
}
class ToggleNodeHighlightEvent extends UsersEvent {
final TreeNode node;
const ToggleNodeHighlightEvent(this.node);
@override
List<Object?> get props => [node];
}
class ExpandAllNodesEvent extends UsersEvent {
@override
List<Object?> get props => [];
}
class CollapseAllNodesEvent extends UsersEvent {
@override
List<Object?> get props => [];
}
class ClearSelectionsEvent extends UsersEvent {
@override
List<Object?> get props => [];
}
class ToggleNodeCheckEvent extends UsersEvent {
final TreeNode node;
const ToggleNodeCheckEvent(this.node);
@override
List<Object?> get props => [];
}
class ToggleNodeCheck extends UsersEvent {
final TreeNode node;
const ToggleNodeCheck(this.node);
@override
List<Object?> get props => [];
}
class EditUserEvent extends UsersEvent {
const EditUserEvent();
@override
List<Object?> get props => [];
}
@@ -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<Object> get props => [];
}
final class RolePermissionInitial extends UsersState {
@override
List<Object> get props => [];
}
final class ChangeStatusSteps extends UsersState {
@override
List<Object> get props => [];
}
final class UsersLoadingState extends UsersState {
@override
List<Object> get props => [];
}
final class SaveState extends UsersState {
@override
List<Object> get props => [];
}
final class SpacesLoadedState extends UsersState {
const SpacesLoadedState();
@override
List<Object> get props => [];
}
final class ErrorState extends UsersState {
final String message;
const ErrorState(this.message);
@override
List<Object> get props => [message];
}
final class RolesErrorState extends UsersState {
final String message;
const RolesErrorState(this.message);
@override
List<Object> get props => [message];
}
/// automation reports
final class ChangeTapStatus extends UsersState {
final bool select;
const ChangeTapStatus({required this.select});
@override
List<Object> get props => [select];
}
final class BasicsStepValidState extends UsersState {
const BasicsStepValidState();
@override
List<Object> get props => [];
}
class BasicsStepInvalidState extends UsersState {
const BasicsStepInvalidState();
@override
List<Object> get props => [];
}
final class ValidateBasics extends UsersState {
@override
List<Object> get props => [];
}
class UsersLoadedState extends UsersState {
final List<TreeNode> updatedCommunities;
const UsersLoadedState({required this.updatedCommunities});
@override
List<Object> get props => [];
}
@@ -0,0 +1,42 @@
class PermissionOption {
String id;
String title;
bool isChecked;
bool isHighlighted;
List<PermissionOption> subOptions;
PermissionOption({
required this.id,
required this.title,
this.isChecked = false,
this.isHighlighted = false,
this.subOptions = const [],
});
factory PermissionOption.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isChecked': isChecked,
'isHighlighted': isHighlighted,
'subOptions': subOptions.map((sub) => sub.toJson()).toList(),
};
}
}
enum CheckState { none, some, all }
@@ -0,0 +1,17 @@
class TreeNode {
String uuid;
String title;
bool isChecked;
bool isHighlighted;
bool isExpanded;
List<TreeNode> children;
TreeNode({
required this.uuid,
required this.title,
this.isChecked = false,
this.isHighlighted = false,
this.isExpanded = false,
this.children = const [],
});
}
@@ -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<AddNewUserDialog> {
int currentStep = 1;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => UsersBloc()
..add(const LoadCommunityAndSpacesEvent())
..add(const RoleEvent()),
child: BlocConsumer<UsersBloc, UsersState>(
listener: (context, state) {},
builder: (context, state) {
final _blocRole = BlocProvider.of<UsersBloc>(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,
),
),
)
],
),
);
}
}
@@ -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<UsersBloc, UsersState>(builder: (context, state) {
final _blocRole = BlocProvider.of<UsersBloc>(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: "[email protected]")
.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),
],
),
);
});
}
}
@@ -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<UsersBloc>(context);
debugPrint('TreeView constructed with userId = $userId');
return BlocProvider(
create: (_) => UsersBloc(),
// ..add(const LoadCommunityAndSpacesEvent()),
child: BlocConsumer<UsersBloc, UsersState>(
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<TreeNode> 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)));
}
}
@@ -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<DeleteUserDialog> {
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,
),
))),
)),
],
)
],
),
));
}
}
@@ -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<EditUserDialog> {
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<UsersBloc, UsersState>(listener: (context, state) {
if (state is SpacesLoadedState) {
BlocProvider.of<UsersBloc>(context)
.add(GetUserByIdEvent(uuid: widget.userId));
}
}, builder: (context, state) {
final _blocRole = BlocProvider.of<UsersBloc>(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,
),
),
)
],
),
);
}
}
@@ -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<PermissionManagement> {
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,
// ),
// ),
@@ -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<void> showPopUpFilterMenu({
required BuildContext context,
required Function(String value) onSortAtoZ,
required Function(String value) onSortZtoA,
Function()? cancelButton,
required Map<String, bool> checkboxStates,
required RelativeRect position,
Function()? onOkPressed,
List<String>? list,
String? isSelected,
}) async {
await showMenu(
context: context,
position: position,
color: ColorsManager.whiteColors,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
items: <PopupMenuEntry>[
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,
),
],
);
},
),
),
],
);
}
@@ -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<RoleDropdown> {
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<String>(
dropdownColor: ColorsManager.whiteColors,
// alignment: Alignment.,
focusColor: Colors.white,
autofocus: true,
value: selectedRole.isNotEmpty ? selectedRole : null,
items: widget.bloc!.roles.map((role) {
return DropdownMenuItem<String>(
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,
),
),
),
),
),
),
],
),
);
}
}
@@ -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<UsersBloc, UsersState>(builder: (context, state) {
final _blocRole = BlocProvider.of<UsersBloc>(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,
))))
],
),
),
),
],
),
),
);
});
}
}
@@ -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<UsersBloc, UsersState>(builder: (context, state) {
final _blocRole = BlocProvider.of<UsersBloc>(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))))
],
),
),
),
],
),
),
);
});
}
}
@@ -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<UserTableEvent, UserTableState> {
UserTableBloc() : super(TableInitial()) {
on<GetUsers>(_getUsers);
on<ChangeUserStatus>(_changeUserStatus);
on<SortUsersByNameAsc>(_toggleSortUsersByNameAsc);
on<SortUsersByNameDesc>(_toggleSortUsersByNameDesc);
on<DateOldestToNewestEvent>(_toggleSortUsersByDateOldestToNewest);
on<DateNewestToOldestEvent>(_toggleSortUsersByDateNewestToOldest);
on<SearchUsers>(_searchUsers);
on<ChangePage>(_handlePageChange);
on<FilterUsersByRoleEvent>(_filterUsersByRole);
on<FilterUsersByJobEvent>(_filterUsersByJobTitle);
on<FilterUsersByCreatedEvent>(_filterUsersByCreated);
on<FilterUsersByDeActevateEvent>(_filterUserStatus);
on<DeleteUserEvent>(_deleteUser);
on<FilterClearEvent>(_filterClear);
}
int itemsPerPage = 20;
int currentPage = 1;
List<RolesUserModel> users = [];
List<RolesUserModel> initialUsers = [];
String currentSortOrder = '';
String currentSortOrderDate = '';
List<String> roleTypes = [];
List<String> jobTitle = [];
List<String> createdBy = [];
List<String> status = ['active', 'invited', 'disabled'];
Future<void> _getUsers(GetUsers event, Emitter<UserTableState> 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<void> _deleteUser(
DeleteUserEvent event, Emitter<UserTableState> 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<void> _changeUserStatus(
ChangeUserStatus event, Emitter<UserTableState> 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<UserTableState> 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<UserTableState> 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<UserTableState> 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<UserTableState> 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<void> _searchUsers(
SearchUsers event, Emitter<UserTableState> 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<UserTableState> 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<UserTableState> 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<String> selectedRoles = {};
Set<String> selectedJobTitles = {};
Set<String> selectedCreatedBy = {};
Set<String> selectedStatuses = {};
void _filterUsersByRole(
FilterUsersByRoleEvent event, Emitter<UserTableState> 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<UserTableState> 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<UserTableState> 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<UserTableState> 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<UserTableState> emit) {
selectedRoles.clear();
selectedJobTitles.clear();
selectedCreatedBy.clear();
selectedStatuses.clear();
emit(UsersLoadedState(users: initialUsers));
}
void _filterClear(FilterClearEvent event, Emitter<UserTableState> emit) {
selectedRoles.clear();
selectedJobTitles.clear();
selectedCreatedBy.clear();
selectedStatuses.clear();
emit(UsersLoadedState(users: initialUsers));
}
}
@@ -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<Object?> get props => [];
}
class GetUsers extends UserTableEvent {
const GetUsers();
@override
List<Object?> get props => [];
}
class ChangeUserStatus extends UserTableEvent {
final String userId;
final String newStatus;
const ChangeUserStatus({required this.userId, required this.newStatus});
@override
List<Object?> get props => [userId, newStatus];
}
class SortUsersByNameAsc extends UserTableEvent {
const SortUsersByNameAsc();
@override
List<Object?> get props => [];
}
class SortUsersByNameDesc extends UserTableEvent {
const SortUsersByNameDesc();
@override
List<Object?> get props => [];
}
class StoreUsersEvent extends UserTableEvent {
const StoreUsersEvent();
@override
List<Object?> get props => [];
}
class DateNewestToOldestEvent extends UserTableEvent {
const DateNewestToOldestEvent();
@override
List<Object?> get props => [];
}
class DateOldestToNewestEvent extends UserTableEvent {
const DateOldestToNewestEvent();
@override
List<Object?> get props => [];
}
class SearchUsers extends UserTableEvent {
final String query;
SearchUsers(this.query);
@override
List<Object?> get props => [];
}
class ChangePage extends UserTableEvent {
final int pageNumber;
ChangePage(this.pageNumber);
@override
List<Object> get props => [pageNumber];
}
class DeleteUserEvent extends UserTableEvent {
final String userId;
final BuildContext context;
const DeleteUserEvent(this.userId, this.context);
@override
List<Object> get props => [userId, context];
}
class FilterUsersByRoleEvent extends UserTableEvent {
final List<String>? selectedRoles;
final String? sortOrder;
const FilterUsersByRoleEvent({this.selectedRoles, this.sortOrder});
List<Object?> get props => [selectedRoles, sortOrder];
}
class FilterUsersByJobEvent extends UserTableEvent {
final List<String>? selectedJob;
final String? sortOrder;
const FilterUsersByJobEvent({this.selectedJob, this.sortOrder});
List<Object?> get props => [selectedJob, sortOrder];
}
class FilterUsersByCreatedEvent extends UserTableEvent {
final List<String>? selectedCreatedBy;
final String? sortOrder;
const FilterUsersByCreatedEvent({this.selectedCreatedBy, this.sortOrder});
List<Object?> get props => [selectedCreatedBy, sortOrder];
}
class FilterUsersByDeActevateEvent extends UserTableEvent {
final List<String>? selectedActivate;
final String? sortOrder;
const FilterUsersByDeActevateEvent({this.selectedActivate, this.sortOrder});
List<Object?> get props => [selectedActivate, sortOrder];
}
class FilterOptionsEvent extends UserTableEvent {
final String query;
final List<String> fullOptions;
FilterOptionsEvent({required this.query, required this.fullOptions});
@override
List<Object?> get props => [query, fullOptions];
}
class FilterClearEvent extends UserTableEvent {
FilterClearEvent();
@override
List<Object?> get props => [];
}
@@ -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<Object> get props => [];
}
final class RolesLoadingState extends UserTableState {
@override
List<Object> get props => [];
}
final class UsersLoadingState extends UserTableState {
@override
List<Object> get props => [];
}
final class RolesLoadedState extends UserTableState {
@override
List<Object> get props => [];
}
final class UsersLoadedState extends UserTableState {
final List<RolesUserModel> users;
const UsersLoadedState({required this.users});
@override
List<Object> get props => [users];
}
final class ErrorState extends UserTableState {
final String message;
const ErrorState(this.message);
@override
List<Object> get props => [message];
}
/// report state
final class SosReportLoadingState extends UserTableState {
@override
List<Object> get props => [];
}
final class RolesErrorState extends UserTableState {
final String message;
const RolesErrorState(this.message);
@override
List<Object> get props => [message];
}
/// automation reports
final class SosAutomationReportLoadingState extends UserTableState {
@override
List<Object> get props => [];
}
final class SosAutomationReportErrorState extends UserTableState {
final String message;
const SosAutomationReportErrorState(this.message);
@override
List<Object> get props => [message];
}
final class ChangeTapStatus extends UserTableState {
final bool select;
const ChangeTapStatus({required this.select});
@override
List<Object> get props => [select];
}
class FilterOptionsState extends UserTableState {
final List<String> filteredOptions;
const FilterOptionsState(this.filteredOptions);
@override
List<Object> get props => [filteredOptions];
}
@@ -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<void> 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: <PopupMenuEntry>[
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;
// });
});
}
@@ -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<void> 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: <PopupMenuEntry>[
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;
// });
});
}
@@ -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<void> 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: <PopupMenuEntry>[
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;
// });
});
}
@@ -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<String> titles;
final List<List<Widget>> 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<DynamicTableScreen>
with WidgetsBindingObserver {
late List<double> columnWidths;
late double totalWidth;
@override
void initState() {
super.initState();
columnWidths = List<double>.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<double>.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<double>.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<double>.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,
),
);
}),
),
],
);
},
),
),
),
],
),
),
),
);
}
}
@@ -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<UserTableBloc, UserTableState>(
builder: (context, state) {
final screenSize = MediaQuery.of(context).size;
final _blocRole = BlocProvider.of<UserTableBloc>(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<UserTableBloc>()
.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<UserTableBloc>()
.add(const SortUsersByNameAsc());
},
zToaTap: () {
context
.read<UserTableBloc>()
.add(const SortUsersByNameDesc());
},
);
}
if (columnIndex == 2) {
final Map<String, bool> 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<String, bool> 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<UserTableBloc>().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<UserTableBloc>()
.add(const DateNewestToOldestEvent());
},
zToaTap: () {
context
.read<UserTableBloc>()
.add(const DateOldestToNewestEvent());
},
);
}
if (columnIndex == 6) {
final Map<String, bool> 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<String, bool> 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<UserTableBloc>()
.add(const DateNewestToOldestEvent());
},
zToaTap: () {
context
.read<UserTableBloc>()
.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<UserTableBloc>().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<UserTableBloc>()
.add(ChangePage(pageNumber));
},
),
),
],
),
),
],
),
),
);
} else if (state is ErrorState) {
return Center(child: Text(state.message));
} else {
return const Center(child: Text('No data available.'));
}
},
);
}
}
@@ -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,
),
),
],
),
),
);
}
}
@@ -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<RolesPermissionBloc, RolesPermissionState>(
listener: (context, state) {},
builder: (context, state) {
final _blocRole = BlocProvider.of<RolesPermissionBloc>(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<UserTableBloc>(
create: (context) => UserTableBloc()..add(const GetUsers()),
child: UsersPage(),
)
// _blocRole.tapSelect == false
// ? UsersPage(
// blocRole: _blocRole,
// )
// : RolesPage(
// blocRole: _blocRole,
// )
);
},
),
);
}
}
@@ -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',
);
},
),
),
],
);
}
}
@@ -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,
@@ -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<AddDeviceTypeEvent, List<SelectedProduct>> {
AddDeviceTypeBloc(List<SelectedProduct> initialProducts)
: super(initialProducts) {
on<UpdateProductCountEvent>(_onUpdateProductCount);
}
void _onUpdateProductCount(
UpdateProductCountEvent event, Emitter<List<SelectedProduct>> 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());
}
}
}
@@ -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<Object> 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<Object> get props => [productId, count];
}
@@ -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<ProductModel>? products;
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
final List<SelectedProduct>? initialSelectedProducts;
final List<SubspaceModel>? subspaces;
final List<Tag>? spaceTags;
final List<String>? allTags;
final String spaceName;
final Function(List<Tag>,List<SubspaceModel>?)? 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<AddDeviceTypeBloc>().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<bool>(
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<Tag> generateInitialTags({
List<Tag>? spaceTags,
List<SubspaceModel>? subspaces,
}) {
final List<Tag> 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;
}
}
@@ -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<SelectedProduct> 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<AddDeviceTypeBloc>().add(
UpdateProductCountEvent(
productId: product.uuid,
count: newCount,
productName: product.catName,
product: product),
);
},
),
],
),
),
);
}
}
@@ -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<ProductModel>? products;
final int crossAxisCount;
final List<SelectedProduct>? 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<AddDeviceTypeBloc, List<SelectedProduct>>(
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;
}
}
@@ -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<SpaceManagementEvent, SpaceManagementState> {
final CommunitySpaceManagementApi _api;
final ProductApi _productApi;
final SpaceModelManagementApi _spaceModelApi;
List<ProductModel>? _cachedProducts;
SpaceManagementBloc(this._api, this._productApi)
SpaceManagementBloc(this._api, this._productApi, this._spaceModelApi)
: super(SpaceManagementInitial()) {
on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces);
on<UpdateSpacePositionEvent>(_onUpdateSpacePosition);
@@ -26,6 +32,8 @@ class SpaceManagementBloc
on<FetchProductsEvent>(_onFetchProducts);
on<SelectSpaceEvent>(_onSelectSpace);
on<NewCommunityEvent>(_onNewCommunity);
on<BlankStateEvent>(_onBlankState);
on<SpaceModelLoadEvent>(_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<List<SpaceTemplateModel>> fetchSpaceModels(
SpaceManagementState previousState) async {
try {
List<SpaceTemplateModel> allSpaces = [];
List<SpaceTemplateModel> prevSpaceModels = [];
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
prevSpaceModels = List<SpaceTemplateModel>.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<void> _onNewCommunity(
NewCommunityEvent event,
Emitter<SpaceManagementState> 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<void> _onBlankState(
BlankStateEvent event, Emitter<SpaceManagementState> 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<CommunityModel>.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<SpaceManagementState> 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<SpaceManagementState> 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<CommunityModel>.from(
(previousState as dynamic).communities,
);
final spaceModels = List<SpaceTemplateModel>.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<void> _updateLoadedState(
SpaceManagementLoaded previousState,
List<SpaceModel> allSpaces,
String communityUuid,
Emitter<SpaceManagementState> emit,
) {
) async {
var prevSpaceModels = await fetchSpaceModels(previousState);
final communities = List<CommunityModel>.from(previousState.communities);
for (var community in communities) {
if (community.uuid == communityUuid) {
community.spaces = allSpaces;
emit(SpaceManagementLoaded(
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: community,
selectedSpace: null,
));
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<CreateTagBodyModel> 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<SpaceManagementState> emit) async {
emit(SpaceManagementLoading());
try {
var prevState = state;
List<CommunityModel> communities = await _api.fetchCommunities();
List<CommunityModel> updatedCommunities = await Future.wait(
communities.map((community) async {
List<SpaceModel> 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'));
}
}
}
@@ -140,3 +140,8 @@ class LoadSpaceHierarchyEvent extends SpaceManagementEvent {
@override
List<Object> get props => [communityId];
}
class BlankStateEvent extends SpaceManagementEvent {}
class SpaceModelLoadEvent extends SpaceManagementEvent {}
@@ -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<ProductModel> products;
CommunityModel? selectedCommunity;
SpaceModel? selectedSpace;
List<SpaceTemplateModel>? 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<CommunityModel> communities;
final List<ProductModel> products;
List<SpaceTemplateModel>? 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<Object> get props => [errorMessage];
}
class SpaceModelLoaded extends SpaceManagementState {
List<SpaceTemplateModel> spaceModels;
final List<ProductModel> products;
final List<CommunityModel> communities;
SpaceModelLoaded({
required this.communities,
required this.products,
required this.spaceModels,
});
@override
List<Object> get props => [communities, products, spaceModels];
}
@@ -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<CreateTagBodyModel>? tags;
Map<String, dynamic> toJson() {
return {
'subspaceName': subspaceName,
'tags': tags?.map((tag) => tag.toJson()).toList(),
};
}
}
@@ -19,7 +19,6 @@ class ProductModel {
// Factory method to create a Product from JSON
factory ProductModel.fromMap(Map<String, dynamic> 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;
}
@@ -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<String, dynamic> toJson() {
return {
'productId': productId,
'count': count,
'productName': productName,
};
}
@@ -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<SelectedProduct> selectedProducts;
String internalId;
SpaceTemplateModel? spaceModel;
List<Tag>? tags;
List<SubspaceModel>? subspaces;
List<Connection> 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<String, dynamic> json,
@@ -64,6 +70,11 @@ class SpaceModel {
name: json['spaceName'],
isPrivate: json['isPrivate'] ?? false,
invitationCode: json['invitationCode'],
subspaces: (json['subspaces'] as List<dynamic>?)
?.where((e) => e is Map<String, dynamic>) // Validate type
.map((e) => SubspaceModel.fromJson(e as Map<String, dynamic>))
.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<dynamic>?)
?.where((item) => item is Map<String, dynamic>) // Validate type
.map((item) => Tag.fromJson(item as Map<String, dynamic>))
.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<String> listAllTagValues() {
final List<String> 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;
}
}
@@ -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<Tag>? tags;
SubspaceModel({
this.uuid,
required this.subspaceName,
required this.disabled,
this.tags,
});
factory SubspaceModel.fromJson(Map<String, dynamic> json) {
return SubspaceModel(
uuid: json['uuid'] ?? '',
subspaceName: json['subspaceName'] ?? '',
disabled: json['disabled'] ?? false,
tags: (json['tags'] as List<dynamic>?)
?.map((item) => Tag.fromJson(item))
.toList() ??
[],
);
}
Map<String, dynamic> 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<UpdateTag>? tags;
UpdateSubspaceModel({
required this.action,
required this.uuid,
this.subspaceName,
this.tags,
});
factory UpdateSubspaceModel.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'action': action.value,
'uuid': uuid,
'tag': tag,
'disabled': disabled,
'product': product?.toMap(),
};
}
}
@@ -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<String, dynamic> 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<String, dynamic> 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;
}
}
@@ -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<SpaceManagementPage> {
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
final ProductApi _productApi = ProductApi();
Map<String, List<SpaceModel>> communitySpaces = {};
List<ProductModel> products = [];
bool isProductDataLoaded = false;
@override
void initState() {
super.initState();
}
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<SpaceManagementBloc, SpaceManagementState>(
builder: (context, state) {
if (state is SpaceManagementLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is BlankState) {
return LoadedSpaceView(
communities: state.communities,
selectedCommunity: null,
selectedSpace: null,
products: state.products,
);
} else if (state is SpaceManagementLoaded) {
return LoadedSpaceView(
communities: state.communities,
selectedCommunity: state.selectedCommunity,
selectedSpace: state.selectedSpace,
products: state.products,
);
} else if (state is SpaceManagementError) {
return Center(child: Text('Error: ${state.errorMessage}'));
}
return Container();
}),
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();
},
),
),
);
}
@@ -99,7 +99,7 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
_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<AddDeviceWidget> {
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<AddDeviceWidget> {
_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;
}
@@ -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<String> onNameSubmitted;
final List<CommunityModel> 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<CommunityStructureHeader> createState() =>
@@ -137,10 +140,8 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
],
),
),
if (widget.isSave) ...[
const SizedBox(width: 8),
_buildActionButtons(theme),
],
const SizedBox(width: 8),
_buildActionButtons(theme),
],
),
],
@@ -152,11 +153,19 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
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<CommunityStructureHeader> {
padding: 2.0,
height: buttonHeight,
elevation: 0,
borderColor: Colors.grey.shade300,
borderColor: ColorsManager.lightGrayColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -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<SpaceModel?>? onSpaceSelected;
final List<CommunityModel> communities;
final List<SpaceModel> spaces;
final List<SpaceTemplateModel>? 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<CommunityStructureArea> {
isEditingName: isEditingName,
nameController: _nameController,
onSave: _saveSpaces,
selectedSpace: widget.selectedSpace,
onDelete: _onDelete,
onEditName: () {
setState(() {
@@ -171,7 +177,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
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<CommunityStructureArea> {
builder: (BuildContext context) {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
onCreateSpace: (String name, String icon,
List<SelectedProduct> selectedProducts) {
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? 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<CommunityStructureArea> {
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<CommunityStructureArea> {
icon: space.icon,
editSpace: space,
isEdit: true,
selectedProducts: space.selectedProducts,
onCreateSpace: (String name, String icon,
List<SelectedProduct> selectedProducts) {
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? 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<CommunityStructureArea> {
List<SpaceModel> 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<CommunityStructureArea> {
}
void _onDelete() {
if (widget.selectedCommunity != null &&
widget.selectedCommunity?.uuid != null &&
widget.selectedSpace == null) {
context.read<SpaceManagementBloc>().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<CommunityStructureArea> {
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<CommunityStructureArea> {
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;
});
}
@@ -4,12 +4,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
class CounterWidget extends StatefulWidget {
final int initialCount;
final ValueChanged<int> 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<CounterWidget> createState() => _CounterWidgetState();
@@ -53,25 +55,26 @@ class _CounterWidgetState extends State<CounterWidget> {
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,
),
);
@@ -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<SelectedProduct> selectedProducts)
onCreateSpace;
final Function(String, String, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) onCreateSpace;
final List<ProductModel>? products;
final String? name;
final String? icon;
@@ -22,6 +26,9 @@ class CreateSpaceDialog extends StatefulWidget {
final List<SelectedProduct> selectedProducts;
final SpaceModel? parentSpace;
final SpaceModel? editSpace;
final List<SpaceTemplateModel>? spaceModels;
final List<SubspaceModel>? subspaces;
final List<Tag>? 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<CreateSpaceDialog> {
String selectedIcon = Assets.location;
SpaceTemplateModel? selectedSpaceModel;
String enteredName = '';
List<SelectedProduct> selectedProducts = [];
late TextEditingController nameController;
bool isOkButtonEnabled = false;
bool isNameFieldInvalid = false;
bool isNameFieldExist = false;
List<SubspaceModel>? subspaces;
List<Tag>? tags;
@override
void initState() {
@@ -58,196 +71,485 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
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<CreateSpaceDialog> {
? 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<CreateSpaceDialog> {
);
}
Widget _buildSelectedProductsButtons(List<ProductModel> 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<CreateSpaceDialog> {
false);
}
String _mapIconToProduct(String uuid, List<ProductModel> 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<Tag>? spaceTags,
bool isEdit,
List<ProductModel>? products,
final List<SubspaceModel>? 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<ProductModel>? 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<SelectedProduct> createInitialSelectedProducts(
List<Tag>? tags, List<SubspaceModel>? subspaces) {
final Map<ProductModel, int> 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<ProductModel, int> _groupTags(List<Tag> tags) {
final Map<ProductModel, int> groupedTags = {};
for (var tag in tags) {
if (tag.product != null) {
groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1;
}
}
return groupedTags;
}
}
@@ -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),
);
}
@@ -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
),
@@ -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<CommunityModel> communities;
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
final List<ProductModel>? products;
final List<SpaceTemplateModel>? 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<LoadedSpaceView> {
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Row(
children: [
SidebarWidget(
communities: widget.communities,
selectedSpaceUuid: widget.selectedSpace?.uuid ??
widget.selectedCommunity?.uuid ??
'',
),
CommunityStructureArea(
selectedCommunity: widget.selectedCommunity,
selectedSpace: widget.selectedSpace,
spaces: widget.selectedCommunity?.spaces ?? [],
products: widget.products,
communities: widget.communities,
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(),
@@ -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),
),
),
);
@@ -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<ProductModel> products;
final List<SelectedProduct> selectedProducts;
final Function(List<SelectedProduct>) 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<Widget> _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<ProductModel> 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;
}
}
@@ -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<SidebarWidget> {
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<SidebarWidget> {
_selectedSpaceUuid = null; // Update the selected community
});
context.read<CenterBodyBloc>().add(CommunitySelectedEvent());
context.read<SpaceManagementBloc>().add(
SelectCommunityEvent(selectedCommunity: community),
);
@@ -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
@@ -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)),
],
@@ -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<AssignTagEvent, AssignTagState> {
AssignTagBloc() : super(AssignTagInitial()) {
on<InitializeTags>((event, emit) {
final initialTags = event.initialTags ?? [];
final existingTagCounts = <String, int>{};
for (var tag in initialTags) {
if (tag.product != null) {
existingTagCounts[tag.product!.uuid] =
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
}
}
final allTags = <Tag>[];
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<UpdateTagEvent>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags);
tags[event.index].tag = event.tag;
emit(AssignTagLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
));
}
});
on<UpdateLocation>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.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<ValidateTags>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags);
emit(AssignTagLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
));
}
});
on<DeleteTag>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final updatedTags = List<Tag>.from(currentState.tags)
..remove(event.tagToDelete);
emit(AssignTagLoaded(
tags: updatedTags,
isSaveEnabled: _validateTags(updatedTags),
));
} else {
emit(const AssignTagLoaded(
tags: [],
isSaveEnabled: false,
));
}
});
}
bool _validateTags(List<Tag> 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<Tag> 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<String, int>>({}, (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;
}
}
@@ -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<Object> get props => [];
}
class InitializeTags extends AssignTagEvent {
final List<Tag>? initialTags;
final List<SelectedProduct> addedProducts;
const InitializeTags({
required this.initialTags,
required this.addedProducts,
});
@override
List<Object> get props => [initialTags ?? [], addedProducts];
}
class UpdateTagEvent extends AssignTagEvent {
final int index;
final String tag;
const UpdateTagEvent({required this.index, required this.tag});
@override
List<Object> get props => [index, tag];
}
class UpdateLocation extends AssignTagEvent {
final int index;
final String location;
const UpdateLocation({required this.index, required this.location});
@override
List<Object> get props => [index, location];
}
class ValidateTags extends AssignTagEvent {}
class DeleteTag extends AssignTagEvent {
final Tag tagToDelete;
final List<Tag> tags;
const DeleteTag({required this.tagToDelete, required this.tags});
@override
List<Object> get props => [tagToDelete, tags];
}
@@ -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<Object> get props => [];
}
class AssignTagInitial extends AssignTagState {}
class AssignTagLoading extends AssignTagState {}
class AssignTagLoaded extends AssignTagState {
final List<Tag> tags;
final bool isSaveEnabled;
final String? errorMessage;
const AssignTagLoaded({
required this.tags,
required this.isSaveEnabled,
this.errorMessage,
});
@override
List<Object> get props => [tags, isSaveEnabled];
}
class AssignTagError extends AssignTagState {
final String errorMessage;
const AssignTagError(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}
@@ -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<ProductModel>? products;
final List<SubspaceModel>? subspaces;
final List<Tag>? initialTags;
final ValueChanged<List<Tag>>? onTagsAssigned;
final List<SelectedProduct> addedProducts;
final List<String>? allTags;
final String spaceName;
final String title;
final Function(List<Tag>, List<SubspaceModel>?)? 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<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList();
return BlocProvider(
create: (_) => AssignTagBloc()
..add(InitializeTags(
initialTags: initialTags,
addedProducts: addedProducts,
)),
child: BlocBuilder<AssignTagBloc, AssignTagState>(
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<AssignTagBloc>().add(
DeleteTag(
tagToDelete: tag,
tags: state.tags));
},
tooltip: 'Delete Tag',
)
],
),
),
DataCell(
Row(
children: [
Expanded(
child: TextFormField(
controller: controller,
onChanged: (value) {
context
.read<AssignTagBloc>()
.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<String>(
color: ColorsManager.whiteColors,
icon: const Icon(
Icons.arrow_drop_down,
color:
ColorsManager.blackColor),
onSelected: (value) {
controller.text = value;
context
.read<AssignTagBloc>()
.add(UpdateTagEvent(
index: index,
tag: value,
));
},
itemBuilder: (context) {
return (allTags ?? [])
.where((tagValue) => !state
.tags
.map((e) => e.tag)
.contains(tagValue))
.map((tagValue) {
return PopupMenuItem<String>(
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<String>(
value: tag.location ?? 'Main',
dropdownColor: ColorsManager
.whiteColors, // Dropdown background
style: const TextStyle(
color: Colors
.black), // Style for selected text
items: [
const DropdownMenuItem<String>(
value: 'Main Space',
child: Text(
'Main Space',
style: TextStyle(
color: ColorsManager
.textPrimaryColor),
),
),
...locations.map((location) {
return DropdownMenuItem<String>(
value: location,
child: Text(
location,
style: const TextStyle(
color: ColorsManager
.textPrimaryColor),
),
);
}).toList(),
],
onChanged: (value) {
if (value != null) {
context
.read<AssignTagBloc>()
.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 = <Tag>{};
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.'));
}
},
),
);
}
}
@@ -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<AssignTagModelEvent, AssignTagModelState> {
AssignTagModelBloc() : super(AssignTagModelInitial()) {
on<InitializeTagModels>((event, emit) {
final initialTags = event.initialTags ?? [];
final existingTagCounts = <String, int>{};
for (var tag in initialTags) {
if (tag.product != null) {
existingTagCounts[tag.product!.uuid] =
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
}
}
final allTags = <TagModel>[];
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<UpdateTag>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
final tags = List<TagModel>.from(currentState.tags);
tags[event.index].tag = event.tag;
emit(AssignTagModelLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
));
}
});
on<UpdateLocation>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
final tags = List<TagModel>.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<ValidateTagModels>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
final tags = List<TagModel>.from(currentState.tags);
emit(AssignTagModelLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
));
}
});
on<DeleteTagModel>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
final updatedTags = List<TagModel>.from(currentState.tags)
..remove(event.tagToDelete);
emit(AssignTagModelLoaded(
tags: updatedTags,
isSaveEnabled: _validateTags(updatedTags),
));
} else {
emit(const AssignTagModelLoaded(
tags: [],
isSaveEnabled: false,
));
}
});
}
bool _validateTags(List<TagModel> 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<TagModel> 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<String, int>>({}, (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;
}
}
@@ -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<Object> get props => [];
}
class InitializeTagModels extends AssignTagModelEvent {
final List<TagModel> initialTags;
final List<SelectedProduct> addedProducts;
const InitializeTagModels({
this.initialTags = const [],
required this.addedProducts,
});
@override
List<Object> get props => [initialTags, addedProducts];
}
class UpdateTag extends AssignTagModelEvent {
final int index;
final String tag;
const UpdateTag({required this.index, required this.tag});
@override
List<Object> get props => [index, tag];
}
class UpdateLocation extends AssignTagModelEvent {
final int index;
final String location;
const UpdateLocation({required this.index, required this.location});
@override
List<Object> get props => [index, location];
}
class ValidateTagModels extends AssignTagModelEvent {}
class DeleteTagModel extends AssignTagModelEvent {
final TagModel tagToDelete;
final List<TagModel> tags;
const DeleteTagModel({required this.tagToDelete, required this.tags});
@override
List<Object> get props => [tagToDelete, tags];
}
@@ -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<Object?> get props => [];
}
class AssignTagModelInitial extends AssignTagModelState {}
class AssignTagModelLoading extends AssignTagModelState {}
class AssignTagModelLoaded extends AssignTagModelState {
final List<TagModel> tags;
final bool isSaveEnabled;
final String? errorMessage;
const AssignTagModelLoaded({
required this.tags,
required this.isSaveEnabled,
this.errorMessage,
});
@override
List<Object?> get props => [tags, isSaveEnabled, errorMessage];
}
class AssignTagModelError extends AssignTagModelState {
final String errorMessage;
const AssignTagModelError(this.errorMessage);
@override
List<Object?> get props => [errorMessage];
}
@@ -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<ProductModel>? products;
final List<SubspaceTemplateModel>? subspaces;
final SpaceTemplateModel? spaceModel;
final List<TagModel> initialTags;
final ValueChanged<List<TagModel>>? onTagsAssigned;
final List<SelectedProduct> addedProducts;
final List<String>? allTags;
final String spaceName;
final String title;
final BuildContext? pageContext;
final List<String>? 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<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider(
create: (_) => AssignTagModelBloc()
..add(InitializeTagModels(
initialTags: initialTags,
addedProducts: addedProducts,
)),
child: BlocListener<AssignTagModelBloc, AssignTagModelState>(
listener: (context, state) {},
child: BlocBuilder<AssignTagModelBloc, AssignTagModelState>(
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<bool>(
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<String> getAvailableTags(
List<String> allTags, List<TagModel> 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<SubspaceTemplateModel>? 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<SubspaceTemplateModel> 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;
}
}
}
@@ -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<SubSpaceEvent, SubSpaceState> {
SubSpaceBloc() : super(SubSpaceState([], [], '')) {
on<AddSubSpace>((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<SubspaceModel>.from(state.subSpaces)
..add(event.subSpace);
emit(SubSpaceState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'',
));
}
});
// Handle RemoveSubSpace Event
on<RemoveSubSpace>((event, emit) {
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
..remove(event.subSpace);
final updatedSubspaceModels = List<UpdateSubspaceModel>.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
}
}
@@ -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);
}
@@ -0,0 +1,26 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
class SubSpaceState {
final List<SubspaceModel> subSpaces;
final List<UpdateSubspaceModel> updatedSubSpaceModels;
final String errorMessage;
SubSpaceState(
this.subSpaces,
this.updatedSubSpaceModels,
this.errorMessage,
);
SubSpaceState copyWith({
List<SubspaceModel>? subSpaces,
List<UpdateSubspaceModel>? updatedSubSpaceModels,
String? errorMessage,
}) {
return SubSpaceState(
subSpaces ?? this.subSpaces,
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
errorMessage ?? this.errorMessage,
);
}
}
@@ -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<SubspaceModel>? existingSubSpaces;
final String? spaceName;
final List<Tag>? spaceTags;
final List<ProductModel>? products;
final Function(List<SubspaceModel>?)? 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<SubSpaceBloc, SubSpaceState>(
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<SubSpaceBloc>()
.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<SubSpaceBloc>().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<SubSpaceBloc>()
.state
.subSpaces;
onSave!(subSpaces);
Navigator.of(context).pop();
},
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
],
),
],
),
),
));
},
),
),
);
}
}
@@ -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<SubSpaceModelEvent, SubSpaceModelState> {
SubSpaceModelBloc() : super(SubSpaceModelState([], [], '', {})) {
// Handle AddSubSpaceModel Event
on<AddSubSpaceModel>((event, emit) {
final existingNames =
state.subSpaces.map((e) => e.subspaceName.toLowerCase()).toSet();
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
final updatedDuplicates = Set<String>.from(state.duplicates)
..add(event.subSpace.subspaceName.toLowerCase());
final updatedSubSpaces =
List<SubspaceTemplateModel>.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<SubspaceTemplateModel>.from(state.subSpaces)
..add(event.subSpace);
emit(SubSpaceModelState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'',
state.duplicates,
// Clear error message
));
}
});
// Handle RemoveSubSpaceModel Event
on<RemoveSubSpaceModel>((event, emit) {
final updatedSubSpaces = List<SubspaceTemplateModel>.from(state.subSpaces)
..remove(event.subSpace);
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
state.updatedSubSpaceModels,
);
final nameOccurrences = <String, int>{};
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<UpdateSubSpaceModel>((event, emit) {
final updatedSubSpaces = state.subSpaces.map((subSpace) {
if (subSpace.uuid == event.updatedSubSpace.uuid) {
return event.updatedSubSpace;
}
return subSpace;
}).toList();
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
state.updatedSubSpaceModels,
);
updatedSubspaceModels.add(UpdateSubspaceTemplateModel(
action: Action.update,
uuid: event.updatedSubSpace.uuid!,
));
emit(SubSpaceModelState(
updatedSubSpaces,
updatedSubspaceModels,
'',
state.duplicates,
// Clear error message
));
});
}
}
@@ -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);
}
@@ -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<SubspaceTemplateModel> subSpaces;
final List<UpdateSubspaceTemplateModel> updatedSubSpaceModels;
final String errorMessage;
final Set<String> duplicates;
SubSpaceModelState(
this.subSpaces,
this.updatedSubSpaceModels,
this.errorMessage,
this.duplicates,
);
SubSpaceModelState copyWith({
List<SubspaceTemplateModel>? subSpaces,
List<UpdateSubspaceTemplateModel>? updatedSubSpaceModels,
String? errorMessage,
Set<String>? duplicates,
}) {
return SubSpaceModelState(
subSpaces ?? this.subSpaces,
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
errorMessage ?? this.errorMessage,
duplicates ?? this.duplicates,
);
}
}
@@ -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<SubspaceTemplateModel>? existingSubSpaces;
final void Function(List<SubspaceTemplateModel> 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<SubSpaceModelBloc, SubSpaceModelState>(
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<SubSpaceModelBloc>()
.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<SubSpaceModelBloc>().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<SubSpaceModelBloc>()
.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'),
),
),
],
),
],
),
),
));
},
),
),
);
}
}
@@ -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<TagModel> generateInitialTags({
List<TagModel>? spaceTagModels,
List<SubspaceTemplateModel>? subspaces,
}) {
final List<TagModel> 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<ProductModel, int> groupTags(List<TagModel> tags) {
final Map<ProductModel, int> groupedTags = {};
for (var tag in tags) {
if (tag.product != null) {
final product = tag.product!;
groupedTags[product] = (groupedTags[product] ?? 0) + 1;
}
}
return groupedTags;
}
static List<SelectedProduct> createInitialSelectedProducts(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces) {
final Map<ProductModel, int> 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();
}
}
@@ -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<SpaceModelEvent, SpaceModelState> {
SpaceModelBloc() : super(SpaceModelInitial()) {
on<SpaceModelSelectedEvent>((event, emit) {
emit(SpaceModelSelectedState(event.selectedIndex));
});
}
}
@@ -0,0 +1,7 @@
abstract class SpaceModelEvent {}
class SpaceModelSelectedEvent extends SpaceModelEvent {
final int selectedIndex;
SpaceModelSelectedEvent(this.selectedIndex);
}
@@ -0,0 +1,9 @@
abstract class SpaceModelState {}
class SpaceModelInitial extends SpaceModelState {}
class SpaceModelSelectedState extends SpaceModelState {
final int selectedIndex;
SpaceModelSelectedState(this.selectedIndex);
}
@@ -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<SpaceTemplateModel> 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<SpaceModelBloc>();
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<SpaceModelBloc, SpaceModelState>(
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<SpaceModelBloc, SpaceModelState>(
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'),
),
);
},
),
],
),
),
],
);
},
),
);
}
}
@@ -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<CreateSpaceModelEvent, CreateSpaceModelState> {
SpaceTemplateModel? _space;
final SpaceModelManagementApi _api;
CreateSpaceModelBloc(this._api) : super(CreateSpaceModelInitial()) {
on<CreateSpaceTemplate>((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<LoadSpaceTemplate>((event, emit) {
emit(CreateSpaceModelLoading());
Future.delayed(const Duration(seconds: 1), () {
if (_space != null) {
emit(CreateSpaceModelLoaded(_space!));
} else {
emit(CreateSpaceModelError("No space template found"));
}
});
});
on<UpdateSpaceTemplate>((event, emit) {
_space = event.spaceTemplate;
emit(CreateSpaceModelLoaded(_space!));
});
on<AddSubspacesToSpaceTemplate>((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<TagModel>((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;
}) ??
<TagModel>[],
...?matchingEventSubspace.tags?.where(
(e) =>
subspace.tags
?.every((t) => t.internalId != e.internalId) ??
true,
) ??
<TagModel>[],
];
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<AddTagsToSpaceTemplate>((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<UpdateSpaceTemplateName>((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<ModifySpaceTemplate>((event, emit) async {
try {
final prevSpaceModel = event.spaceTemplate;
final newSpaceModel = event.updatedSpaceTemplate;
String? spaceModelName;
if (prevSpaceModel.modelName != newSpaceModel.modelName) {
spaceModelName = newSpaceModel.modelName;
}
List<TagModelUpdate> tagUpdates = [];
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
final List<SubspaceTemplateModel>? prevSubspaces =
prevSpaceModel.subspaceModels;
final List<SubspaceTemplateModel>? 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<TagModelUpdate> 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<TagModelUpdate> 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<TagModelUpdate> processTagUpdates(
List<TagModel>? prevTags,
List<TagModel>? newTags,
) {
final List<TagModelUpdate> tagUpdates = [];
final processedTags = <String?>{};
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;
}
}
@@ -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<Object> 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<Object> get props => [spaceTemplate];
}
class UpdateSpaceTemplateName extends CreateSpaceModelEvent {
final String name;
final List<String> allModels;
UpdateSpaceTemplateName({required this.name, required this.allModels});
@override
List<Object> get props => [name, allModels];
}
class AddSubspacesToSpaceTemplate extends CreateSpaceModelEvent {
final List<SubspaceTemplateModel> subspaces;
AddSubspacesToSpaceTemplate(this.subspaces);
}
class AddTagsToSpaceTemplate extends CreateSpaceModelEvent {
final List<TagModel> 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});
}
@@ -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<Object?> 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<Object?> get props => [space, errorMessage];
}
class CreateSpaceModelError extends CreateSpaceModelState {
final String message;
CreateSpaceModelError(this.message);
}
@@ -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<SpaceModelEvent, SpaceModelState> {
final SpaceModelManagementApi api;
SpaceModelBloc({
required this.api,
required List<SpaceTemplateModel> initialSpaceModels,
}) : super(SpaceModelLoaded(spaceModels: initialSpaceModels)) {
on<CreateSpaceModel>(_onCreateSpaceModel);
on<UpdateSpaceModel>(_onUpdateSpaceModel);
}
Future<void> _onCreateSpaceModel(
CreateSpaceModel event, Emitter<SpaceModelState> emit) async {
final currentState = state;
if (currentState is SpaceModelLoaded) {
try {
final newSpaceModel =
await api.getSpaceModel(event.newSpaceModel.uuid ?? '');
if (newSpaceModel != null) {
final updatedSpaceModels =
List<SpaceTemplateModel>.from(currentState.spaceModels)
..add(newSpaceModel);
emit(SpaceModelLoaded(spaceModels: updatedSpaceModels));
}
} catch (e) {
emit(SpaceModelError(message: e.toString()));
}
}
}
Future<void> _onUpdateSpaceModel(
UpdateSpaceModel event, Emitter<SpaceModelState> 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()));
}
}
}
}
@@ -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<Object?> get props => [];
}
class LoadSpaceModels extends SpaceModelEvent {}
class CreateSpaceModel extends SpaceModelEvent {
final SpaceTemplateModel newSpaceModel;
CreateSpaceModel({required this.newSpaceModel});
@override
List<Object?> get props => [newSpaceModel];
}
class GetSpaceModel extends SpaceModelEvent {
final String spaceModelUuid;
GetSpaceModel({required this.spaceModelUuid});
@override
List<Object?> get props => [spaceModelUuid];
}
class UpdateSpaceModel extends SpaceModelEvent {
final String spaceModelUuid;
UpdateSpaceModel({required this.spaceModelUuid});
@override
List<Object?> get props => [spaceModelUuid];
}
@@ -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<Object?> get props => [];
}
class SpaceModelInitial extends SpaceModelState {}
class SpaceModelLoading extends SpaceModelState {}
class SpaceModelLoaded extends SpaceModelState {
final List<SpaceTemplateModel> spaceModels;
SpaceModelLoaded({required this.spaceModels});
@override
List<Object?> get props => [spaceModels];
}
class SpaceModelError extends SpaceModelState {
final String message;
SpaceModelError({required this.message});
@override
List<Object?> get props => [message];
}

Some files were not shown because too many files have changed in this diff Show More