From 0d79d5a5149736edfbf5de7b36128994d0065e11 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Sat, 20 Jun 2026 15:50:44 +0530 Subject: [PATCH] screen slide issue and add role --- .../providers/role_form_provider.dart | 45 ++++++++++- .../widgets/create_role_panel.dart | 59 ++++++++++---- .../datasources/role_remote_data_source.dart | 3 +- .../models/permission_matrix_models.dart | 43 ++++++++-- lib/shared/models/user_management_models.dart | 2 + .../user_management_models.freezed.dart | 79 ++++++++++++++++++- .../models/user_management_models.g.dart | 12 +++ lib/shared/routes/app_router.dart | 62 ++++++++++----- lib/shared/widgets/app_shell.dart | 29 ++++--- 9 files changed, 276 insertions(+), 58 deletions(-) diff --git a/lib/modules/rbac/presentation/providers/role_form_provider.dart b/lib/modules/rbac/presentation/providers/role_form_provider.dart index c89cdff..f7292d8 100644 --- a/lib/modules/rbac/presentation/providers/role_form_provider.dart +++ b/lib/modules/rbac/presentation/providers/role_form_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../shared/models/permission_matrix_models.dart'; import '../../../../shared/models/user_management_models.dart'; import '../../../roles/data/repositories/role_repository_impl.dart'; import '../../../roles/domain/usecases/role_usecases.dart'; @@ -8,28 +9,64 @@ import '../../../roles/presentation/providers/roles_provider.dart'; class RoleFormState { const RoleFormState({ this.editingRole, + this.permissionCatalog = const [], this.isSubmitting = false, this.errorMessage, }); final RoleCardModel? editingRole; + final List permissionCatalog; final bool isSubmitting; final String? errorMessage; + /// Modules that expose a view permission in the live catalog. + List get selectableModules => permissionCatalog + .where((module) => module.permissionIdForAction('view') != null) + .toList(); + RoleFormState copyWith({ RoleCardModel? editingRole, + List? permissionCatalog, bool? isSubmitting, String? errorMessage, bool clearError = false, }) { return RoleFormState( editingRole: editingRole ?? this.editingRole, + permissionCatalog: permissionCatalog ?? this.permissionCatalog, isSubmitting: isSubmitting ?? this.isSubmitting, errorMessage: clearError ? null : errorMessage ?? this.errorMessage, ); } } +CreateRoleRequest buildCreateRoleRequest({ + required String name, + required String description, + required Map selectedModules, + required List catalog, +}) { + final viewModules = []; + final permissionIds = []; + + for (final module in catalog) { + if (selectedModules[module.code] != true) continue; + + final viewPermissionId = module.permissionIdForAction('view'); + if (viewPermissionId == null) continue; + + viewModules.add(module.code); + permissionIds.add(viewPermissionId); + } + + return CreateRoleRequest( + name: name, + description: description.isEmpty ? null : description, + permissionIds: permissionIds, + viewModules: viewModules, + ); +} + final createRoleUseCaseProvider = Provider( (ref) => CreateRoleUseCase(ref.watch(roleRepositoryProvider)), ); @@ -46,7 +83,13 @@ final roleFormProvider = class RoleFormNotifier extends FamilyAsyncNotifier { @override Future build(String? roleId) async { - if (roleId == null) return const RoleFormState(); + if (roleId == null) { + final catalogResult = await ref.read(getPermissionCatalogUseCaseProvider)(); + if (catalogResult.failure != null) throw catalogResult.failure!; + return RoleFormState( + permissionCatalog: catalogResult.data ?? const [], + ); + } final cached = ref.read(rolesListProvider).valueOrNull?.roles; var editingRole = cached?.where((role) => role.id == roleId).firstOrNull; diff --git a/lib/modules/rbac/presentation/widgets/create_role_panel.dart b/lib/modules/rbac/presentation/widgets/create_role_panel.dart index 7d73dcb..7aabaa1 100644 --- a/lib/modules/rbac/presentation/widgets/create_role_panel.dart +++ b/lib/modules/rbac/presentation/widgets/create_role_panel.dart @@ -3,12 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/errors/failure.dart'; import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/permission_matrix_models.dart'; import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/error_view.dart'; -import '../../domain/entities/rbac_entities.dart'; import '../providers/role_form_provider.dart'; import 'rbac_widgets.dart'; @@ -27,10 +27,9 @@ class _RoleFormPanelState extends ConsumerState { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _descriptionController = TextEditingController(); - final Map _viewPermissions = { - for (final module in rbacModules) module.key: false, - }; + final Map _viewPermissions = {}; bool _prefilled = false; + bool _permissionsInitialized = false; @override void dispose() { @@ -49,6 +48,9 @@ class _RoleFormPanelState extends ConsumerState { Future _save() async { if (!_formKey.currentState!.validate()) return; + final formState = ref.read(roleFormProvider(widget.roleId)).valueOrNull; + if (formState == null) return; + final notifier = ref.read(roleFormProvider(widget.roleId).notifier); final bool success; @@ -62,9 +64,11 @@ class _RoleFormPanelState extends ConsumerState { ); } else { success = await notifier.submitCreate( - CreateRoleRequest( + buildCreateRoleRequest( name: _nameController.text.trim(), description: _descriptionController.text.trim(), + selectedModules: _viewPermissions, + catalog: formState.permissionCatalog, ), ); } @@ -124,6 +128,20 @@ class _RoleFormPanelState extends ConsumerState { onRetry: () => ref.invalidate(roleFormProvider(widget.roleId)), ), data: (formState) { + if (!widget.isEditing && + !_permissionsInitialized && + formState.selectableModules.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _permissionsInitialized) return; + setState(() { + _permissionsInitialized = true; + for (final module in formState.selectableModules) { + _viewPermissions.putIfAbsent(module.code, () => false); + } + }); + }); + } + if (formState.editingRole != null && !_prefilled) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || _prefilled) return; @@ -167,16 +185,27 @@ class _RoleFormPanelState extends ConsumerState { ), ), const SizedBox(height: 16), - ...rbacModules.map((module) { - return ModulePermissionRow( - icon: module.icon, - color: module.color, - label: module.label, - value: _viewPermissions[module.key] ?? false, - onChanged: (v) => - setState(() => _viewPermissions[module.key] = v), - ); - }), + if (formState.selectableModules.isEmpty) + Text( + 'No view permissions available from the server.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + else + ...formState.selectableModules.asMap().entries.map((entry) { + final module = entry.value; + final appearance = + permissionModuleAppearance(module.code, entry.key); + return ModulePermissionRow( + icon: appearance.icon, + color: appearance.color, + label: module.name, + value: _viewPermissions[module.code] ?? false, + onChanged: (v) => + setState(() => _viewPermissions[module.code] = v), + ); + }), ], ], ), diff --git a/lib/modules/roles/data/datasources/role_remote_data_source.dart b/lib/modules/roles/data/datasources/role_remote_data_source.dart index 659fc26..3166e74 100644 --- a/lib/modules/roles/data/datasources/role_remote_data_source.dart +++ b/lib/modules/roles/data/datasources/role_remote_data_source.dart @@ -57,7 +57,8 @@ class RoleRemoteDataSource { } Future createRole(CreateRoleRequest request) async { - final response = await dio.post(ApiEndpoints.roles, data: request.toJson()); + final payload = request.toJson()..removeWhere((_, value) => value == null); + final response = await dio.post(ApiEndpoints.roles, data: payload); return _parseRole(response.data); } diff --git a/lib/shared/models/permission_matrix_models.dart b/lib/shared/models/permission_matrix_models.dart index dcba05d..9a2a2f0 100644 --- a/lib/shared/models/permission_matrix_models.dart +++ b/lib/shared/models/permission_matrix_models.dart @@ -10,6 +10,13 @@ const permissionMatrixActionOrder = [ 'export', ]; +class ModulePermissionEntry { + const ModulePermissionEntry({required this.id, required this.action}); + + final String id; + final String action; +} + String permissionActionLabel(String action) => switch (action) { 'view' => 'VIEW', 'create' => 'CREATE', @@ -32,25 +39,47 @@ class PermissionModuleCatalog { required this.code, required this.name, required this.actions, + required this.permissions, }); final String id; final String code; final String name; final List actions; + final List permissions; + + int? permissionIdForAction(String action) { + final normalized = action.toLowerCase(); + for (final permission in permissions) { + if (permission.action.toLowerCase() == normalized) { + return int.tryParse(permission.id); + } + } + return null; + } factory PermissionModuleCatalog.fromJson(Map json) { - final permissions = json['permissions'] as List? ?? const []; - final actions = permissions - .map((item) => (item as Map)['action'] as String?) - .whereType() - .toList(); + final permissions = []; + for (final item in json['permissions'] as List? ?? const []) { + if (item is! Map) continue; + final action = item['action'] as String?; + if (action == null) continue; + permissions.add( + ModulePermissionEntry( + id: item['id']?.toString() ?? '', + action: action, + ), + ); + } + + final actions = permissions.map((entry) => entry.action).toList(); return PermissionModuleCatalog( id: json['id']?.toString() ?? '', code: json['code'] as String? ?? '', name: json['name'] as String? ?? '', actions: sortPermissionActions(actions), + permissions: permissions, ); } } @@ -182,9 +211,9 @@ class RolePermissionMatrix { 'ROLES' => styles[1], 'MASTERS' => styles[2], 'VENDOR' => styles[3], - 'PURCHASE_ORDERS' || 'PO' => styles[4], + 'PURCHASE_ORDERS' || 'PURCHASE_ORDER' || 'PO' => styles[4], 'GRN' => styles[5], - 'ASSETS' => styles[6], + 'ASSETS' || 'ASSET' || 'ASSET_MANAGEMENT' => styles[6], _ => styles[index % styles.length], }; return byCode; diff --git a/lib/shared/models/user_management_models.dart b/lib/shared/models/user_management_models.dart index 04dec36..7bab802 100644 --- a/lib/shared/models/user_management_models.dart +++ b/lib/shared/models/user_management_models.dart @@ -231,6 +231,8 @@ class CreateRoleRequest with _$CreateRoleRequest { required String name, String? description, @JsonKey(name: 'is_active') @Default(true) bool isActive, + @JsonKey(name: 'permission_ids') @Default([]) List permissionIds, + @JsonKey(name: 'view_modules') @Default([]) List viewModules, }) = _CreateRoleRequest; factory CreateRoleRequest.fromJson(Map json) => diff --git a/lib/shared/models/user_management_models.freezed.dart b/lib/shared/models/user_management_models.freezed.dart index 3d046cd..24304d1 100644 --- a/lib/shared/models/user_management_models.freezed.dart +++ b/lib/shared/models/user_management_models.freezed.dart @@ -3011,6 +3011,10 @@ mixin _$CreateRoleRequest { String? get description => throw _privateConstructorUsedError; @JsonKey(name: 'is_active') bool get isActive => throw _privateConstructorUsedError; + @JsonKey(name: 'permission_ids') + List get permissionIds => throw _privateConstructorUsedError; + @JsonKey(name: 'view_modules') + List get viewModules => throw _privateConstructorUsedError; /// Serializes this CreateRoleRequest to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -3033,6 +3037,8 @@ abstract class $CreateRoleRequestCopyWith<$Res> { String name, String? description, @JsonKey(name: 'is_active') bool isActive, + @JsonKey(name: 'permission_ids') List permissionIds, + @JsonKey(name: 'view_modules') List viewModules, }); } @@ -3054,6 +3060,8 @@ class _$CreateRoleRequestCopyWithImpl<$Res, $Val extends CreateRoleRequest> Object? name = null, Object? description = freezed, Object? isActive = null, + Object? permissionIds = null, + Object? viewModules = null, }) { return _then( _value.copyWith( @@ -3069,6 +3077,14 @@ class _$CreateRoleRequestCopyWithImpl<$Res, $Val extends CreateRoleRequest> ? _value.isActive : isActive // ignore: cast_nullable_to_non_nullable as bool, + permissionIds: null == permissionIds + ? _value.permissionIds + : permissionIds // ignore: cast_nullable_to_non_nullable + as List, + viewModules: null == viewModules + ? _value.viewModules + : viewModules // ignore: cast_nullable_to_non_nullable + as List, ) as $Val, ); @@ -3088,6 +3104,8 @@ abstract class _$$CreateRoleRequestImplCopyWith<$Res> String name, String? description, @JsonKey(name: 'is_active') bool isActive, + @JsonKey(name: 'permission_ids') List permissionIds, + @JsonKey(name: 'view_modules') List viewModules, }); } @@ -3108,6 +3126,8 @@ class __$$CreateRoleRequestImplCopyWithImpl<$Res> Object? name = null, Object? description = freezed, Object? isActive = null, + Object? permissionIds = null, + Object? viewModules = null, }) { return _then( _$CreateRoleRequestImpl( @@ -3123,6 +3143,14 @@ class __$$CreateRoleRequestImplCopyWithImpl<$Res> ? _value.isActive : isActive // ignore: cast_nullable_to_non_nullable as bool, + permissionIds: null == permissionIds + ? _value._permissionIds + : permissionIds // ignore: cast_nullable_to_non_nullable + as List, + viewModules: null == viewModules + ? _value._viewModules + : viewModules // ignore: cast_nullable_to_non_nullable + as List, ), ); } @@ -3135,7 +3163,10 @@ class _$CreateRoleRequestImpl implements _CreateRoleRequest { required this.name, this.description, @JsonKey(name: 'is_active') this.isActive = true, - }); + @JsonKey(name: 'permission_ids') final List permissionIds = const [], + @JsonKey(name: 'view_modules') final List viewModules = const [], + }) : _permissionIds = permissionIds, + _viewModules = viewModules; factory _$CreateRoleRequestImpl.fromJson(Map json) => _$$CreateRoleRequestImplFromJson(json); @@ -3147,10 +3178,27 @@ class _$CreateRoleRequestImpl implements _CreateRoleRequest { @override @JsonKey(name: 'is_active') final bool isActive; + final List _permissionIds; + @override + @JsonKey(name: 'permission_ids') + List get permissionIds { + if (_permissionIds is EqualUnmodifiableListView) return _permissionIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_permissionIds); + } + + final List _viewModules; + @override + @JsonKey(name: 'view_modules') + List get viewModules { + if (_viewModules is EqualUnmodifiableListView) return _viewModules; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_viewModules); + } @override String toString() { - return 'CreateRoleRequest(name: $name, description: $description, isActive: $isActive)'; + return 'CreateRoleRequest(name: $name, description: $description, isActive: $isActive, permissionIds: $permissionIds, viewModules: $viewModules)'; } @override @@ -3162,12 +3210,27 @@ class _$CreateRoleRequestImpl implements _CreateRoleRequest { (identical(other.description, description) || other.description == description) && (identical(other.isActive, isActive) || - other.isActive == isActive)); + other.isActive == isActive) && + const DeepCollectionEquality().equals( + other._permissionIds, + _permissionIds, + ) && + const DeepCollectionEquality().equals( + other._viewModules, + _viewModules, + )); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, name, description, isActive); + int get hashCode => Object.hash( + runtimeType, + name, + description, + isActive, + const DeepCollectionEquality().hash(_permissionIds), + const DeepCollectionEquality().hash(_viewModules), + ); /// Create a copy of CreateRoleRequest /// with the given fields replaced by the non-null parameter values. @@ -3191,6 +3254,8 @@ abstract class _CreateRoleRequest implements CreateRoleRequest { required final String name, final String? description, @JsonKey(name: 'is_active') final bool isActive, + @JsonKey(name: 'permission_ids') final List permissionIds, + @JsonKey(name: 'view_modules') final List viewModules, }) = _$CreateRoleRequestImpl; factory _CreateRoleRequest.fromJson(Map json) = @@ -3203,6 +3268,12 @@ abstract class _CreateRoleRequest implements CreateRoleRequest { @override @JsonKey(name: 'is_active') bool get isActive; + @override + @JsonKey(name: 'permission_ids') + List get permissionIds; + @override + @JsonKey(name: 'view_modules') + List get viewModules; /// Create a copy of CreateRoleRequest /// with the given fields replaced by the non-null parameter values. diff --git a/lib/shared/models/user_management_models.g.dart b/lib/shared/models/user_management_models.g.dart index 92a754d..6266fca 100644 --- a/lib/shared/models/user_management_models.g.dart +++ b/lib/shared/models/user_management_models.g.dart @@ -228,6 +228,16 @@ _$CreateRoleRequestImpl _$$CreateRoleRequestImplFromJson( name: json['name'] as String, description: json['description'] as String?, isActive: json['is_active'] as bool? ?? true, + permissionIds: + (json['permission_ids'] as List?) + ?.map((e) => (e as num).toInt()) + .toList() ?? + const [], + viewModules: + (json['view_modules'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], ); Map _$$CreateRoleRequestImplToJson( @@ -236,6 +246,8 @@ Map _$$CreateRoleRequestImplToJson( 'name': instance.name, 'description': instance.description, 'is_active': instance.isActive, + 'permission_ids': instance.permissionIds, + 'view_modules': instance.viewModules, }; _$UpdateRoleRequestImpl _$$UpdateRoleRequestImplFromJson( diff --git a/lib/shared/routes/app_router.dart b/lib/shared/routes/app_router.dart index 41102cc..5600b2c 100644 --- a/lib/shared/routes/app_router.dart +++ b/lib/shared/routes/app_router.dart @@ -118,11 +118,13 @@ final routerProvider = Provider((ref) { routes: [ GoRoute( path: RouteConstants.dashboard, - builder: (context, state) => const DashboardScreen(), + pageBuilder: (context, state) => + shellPage(state, const DashboardScreen()), ), GoRoute( path: RouteConstants.companies, - builder: (context, state) => const CompanyListScreen(), + pageBuilder: (context, state) => + shellPage(state, const CompanyListScreen()), routes: [ GoRoute( path: 'add', @@ -137,7 +139,8 @@ final routerProvider = Provider((ref) { ), GoRoute( path: RouteConstants.branches, - builder: (context, state) => const BranchListScreen(), + pageBuilder: (context, state) => + shellPage(state, const BranchListScreen()), routes: [ GoRoute( path: 'add', @@ -152,14 +155,18 @@ final routerProvider = Provider((ref) { ), GoRoute( path: RouteConstants.usersRoleManagement, - builder: (context, state) => UsersRoleManagementScreen( - initialTab: rbacTabFromLocation(state.uri.toString()), + pageBuilder: (context, state) => shellPage( + state, + UsersRoleManagementScreen( + initialTab: rbacTabFromLocation(state.uri.toString()), + ), ), ), GoRoute( path: RouteConstants.users, - builder: (context, state) => const UsersRoleManagementScreen( - initialTab: RbacTab.users, + pageBuilder: (context, state) => shellPage( + state, + const UsersRoleManagementScreen(initialTab: RbacTab.users), ), routes: [ GoRoute( @@ -180,8 +187,9 @@ final routerProvider = Provider((ref) { ), GoRoute( path: RouteConstants.roles, - builder: (context, state) => const UsersRoleManagementScreen( - initialTab: RbacTab.roles, + pageBuilder: (context, state) => shellPage( + state, + const UsersRoleManagementScreen(initialTab: RbacTab.roles), ), routes: [ GoRoute( @@ -194,11 +202,13 @@ final routerProvider = Provider((ref) { ), GoRoute( path: RouteConstants.profile, - builder: (context, state) => const UserProfileScreen(), + pageBuilder: (context, state) => + shellPage(state, const UserProfileScreen()), ), GoRoute( path: RouteConstants.assets, - builder: (context, state) => const AssetListScreen(), + pageBuilder: (context, state) => + shellPage(state, const AssetListScreen()), routes: [ GoRoute( path: 'add', @@ -242,27 +252,33 @@ final routerProvider = Provider((ref) { ), GoRoute( path: RouteConstants.departments, - builder: (context, state) => const DepartmentsScreen(), + pageBuilder: (context, state) => + shellPage(state, const DepartmentsScreen()), ), GoRoute( path: RouteConstants.locations, - builder: (context, state) => const LocationsScreen(), + pageBuilder: (context, state) => + shellPage(state, const LocationsScreen()), ), GoRoute( path: RouteConstants.uom, - builder: (context, state) => const UomScreen(), + pageBuilder: (context, state) => + shellPage(state, const UomScreen()), ), GoRoute( path: RouteConstants.reports, - builder: (context, state) => const ReportsScreen(), + pageBuilder: (context, state) => + shellPage(state, const ReportsScreen()), ), GoRoute( path: RouteConstants.auditLogs, - builder: (context, state) => const AuditLogsScreen(), + pageBuilder: (context, state) => + shellPage(state, const AuditLogsScreen()), ), GoRoute( path: RouteConstants.settings, - builder: (context, state) => const SettingsScreen(), + pageBuilder: (context, state) => + shellPage(state, const SettingsScreen()), routes: [ GoRoute( path: 'general', @@ -301,7 +317,8 @@ final routerProvider = Provider((ref) { if (DevConfig.screenPreviewEnabled) GoRoute( path: RouteConstants.screenGallery, - builder: (context, state) => const ScreenGalleryScreen(), + pageBuilder: (context, state) => + shellPage(state, const ScreenGalleryScreen()), ), ], ), @@ -319,6 +336,15 @@ Widget _themedRoute(GoRouterState state, Widget child) { ); } +/// Instant swap for main shell pages — prevents old page showing through +/// during sidebar navigation slide transitions. +Page shellPage(GoRouterState state, Widget child) { + return NoTransitionPage( + key: state.pageKey, + child: child, + ); +} + class _AuthListenable extends ChangeNotifier { _AuthListenable(this._ref) { _ref.listen(authStateProvider, (_, __) => notifyListeners()); diff --git a/lib/shared/widgets/app_shell.dart b/lib/shared/widgets/app_shell.dart index 8fe3da2..75d525d 100644 --- a/lib/shared/widgets/app_shell.dart +++ b/lib/shared/widgets/app_shell.dart @@ -30,6 +30,20 @@ class _AppShellState extends ConsumerState { final _scaffoldKey = GlobalKey(); bool _sidebarCollapsed = false; + Widget _pageContent(BuildContext context, String currentRoute, Widget child) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Material( + color: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + child: ThemeKeyedSubtree( + pageKey: currentRoute, + child: child, + ), + ), + ); + } + @override Widget build(BuildContext context) { final authState = ref.watch(authStateProvider); @@ -71,10 +85,7 @@ class _AppShellState extends ConsumerState { _scaffoldKey.currentState?.closeDrawer(); }, ), - body: ThemeKeyedSubtree( - pageKey: currentRoute, - child: widget.child, - ), + body: _pageContent(context, currentRoute, widget.child), ); } @@ -97,10 +108,7 @@ class _AppShellState extends ConsumerState { onItemTap: (route) => goAndDismissOverlays(context, route), ), Expanded( - child: ThemeKeyedSubtree( - pageKey: currentRoute, - child: widget.child, - ), + child: _pageContent(context, currentRoute, widget.child), ), ], ), @@ -126,10 +134,7 @@ class _AppShellState extends ConsumerState { ), const SizedBox(width: 12), Expanded( - child: ThemeKeyedSubtree( - pageKey: currentRoute, - child: widget.child, - ), + child: _pageContent(context, currentRoute, widget.child), ), ], ),