screen slide issue and add role

This commit is contained in:
Surendiran 2026-06-20 15:50:44 +05:30
parent 0102a7405c
commit 0d79d5a514
9 changed files with 276 additions and 58 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../shared/models/permission_matrix_models.dart';
import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/models/user_management_models.dart';
import '../../../roles/data/repositories/role_repository_impl.dart'; import '../../../roles/data/repositories/role_repository_impl.dart';
import '../../../roles/domain/usecases/role_usecases.dart'; import '../../../roles/domain/usecases/role_usecases.dart';
@ -8,28 +9,64 @@ import '../../../roles/presentation/providers/roles_provider.dart';
class RoleFormState { class RoleFormState {
const RoleFormState({ const RoleFormState({
this.editingRole, this.editingRole,
this.permissionCatalog = const [],
this.isSubmitting = false, this.isSubmitting = false,
this.errorMessage, this.errorMessage,
}); });
final RoleCardModel? editingRole; final RoleCardModel? editingRole;
final List<PermissionModuleCatalog> permissionCatalog;
final bool isSubmitting; final bool isSubmitting;
final String? errorMessage; final String? errorMessage;
/// Modules that expose a view permission in the live catalog.
List<PermissionModuleCatalog> get selectableModules => permissionCatalog
.where((module) => module.permissionIdForAction('view') != null)
.toList();
RoleFormState copyWith({ RoleFormState copyWith({
RoleCardModel? editingRole, RoleCardModel? editingRole,
List<PermissionModuleCatalog>? permissionCatalog,
bool? isSubmitting, bool? isSubmitting,
String? errorMessage, String? errorMessage,
bool clearError = false, bool clearError = false,
}) { }) {
return RoleFormState( return RoleFormState(
editingRole: editingRole ?? this.editingRole, editingRole: editingRole ?? this.editingRole,
permissionCatalog: permissionCatalog ?? this.permissionCatalog,
isSubmitting: isSubmitting ?? this.isSubmitting, isSubmitting: isSubmitting ?? this.isSubmitting,
errorMessage: clearError ? null : errorMessage ?? this.errorMessage, errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
); );
} }
} }
CreateRoleRequest buildCreateRoleRequest({
required String name,
required String description,
required Map<String, bool> selectedModules,
required List<PermissionModuleCatalog> catalog,
}) {
final viewModules = <String>[];
final permissionIds = <int>[];
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( final createRoleUseCaseProvider = Provider(
(ref) => CreateRoleUseCase(ref.watch(roleRepositoryProvider)), (ref) => CreateRoleUseCase(ref.watch(roleRepositoryProvider)),
); );
@ -46,7 +83,13 @@ final roleFormProvider =
class RoleFormNotifier extends FamilyAsyncNotifier<RoleFormState, String?> { class RoleFormNotifier extends FamilyAsyncNotifier<RoleFormState, String?> {
@override @override
Future<RoleFormState> build(String? roleId) async { Future<RoleFormState> 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; final cached = ref.read(rolesListProvider).valueOrNull?.roles;
var editingRole = cached?.where((role) => role.id == roleId).firstOrNull; var editingRole = cached?.where((role) => role.id == roleId).firstOrNull;

View File

@ -3,12 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failure.dart'; import '../../../../core/errors/failure.dart';
import '../../../../core/utils/validators.dart'; import '../../../../core/utils/validators.dart';
import '../../../../shared/models/permission_matrix_models.dart';
import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/app_loading_view.dart';
import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/error_view.dart';
import '../../domain/entities/rbac_entities.dart';
import '../providers/role_form_provider.dart'; import '../providers/role_form_provider.dart';
import 'rbac_widgets.dart'; import 'rbac_widgets.dart';
@ -27,10 +27,9 @@ class _RoleFormPanelState extends ConsumerState<RoleFormPanel> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
final Map<String, bool> _viewPermissions = { final Map<String, bool> _viewPermissions = {};
for (final module in rbacModules) module.key: false,
};
bool _prefilled = false; bool _prefilled = false;
bool _permissionsInitialized = false;
@override @override
void dispose() { void dispose() {
@ -49,6 +48,9 @@ class _RoleFormPanelState extends ConsumerState<RoleFormPanel> {
Future<void> _save() async { Future<void> _save() async {
if (!_formKey.currentState!.validate()) return; 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 notifier = ref.read(roleFormProvider(widget.roleId).notifier);
final bool success; final bool success;
@ -62,9 +64,11 @@ class _RoleFormPanelState extends ConsumerState<RoleFormPanel> {
); );
} else { } else {
success = await notifier.submitCreate( success = await notifier.submitCreate(
CreateRoleRequest( buildCreateRoleRequest(
name: _nameController.text.trim(), name: _nameController.text.trim(),
description: _descriptionController.text.trim(), description: _descriptionController.text.trim(),
selectedModules: _viewPermissions,
catalog: formState.permissionCatalog,
), ),
); );
} }
@ -124,6 +128,20 @@ class _RoleFormPanelState extends ConsumerState<RoleFormPanel> {
onRetry: () => ref.invalidate(roleFormProvider(widget.roleId)), onRetry: () => ref.invalidate(roleFormProvider(widget.roleId)),
), ),
data: (formState) { 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) { if (formState.editingRole != null && !_prefilled) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _prefilled) return; if (!mounted || _prefilled) return;
@ -167,16 +185,27 @@ class _RoleFormPanelState extends ConsumerState<RoleFormPanel> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
...rbacModules.map((module) { if (formState.selectableModules.isEmpty)
return ModulePermissionRow( Text(
icon: module.icon, 'No view permissions available from the server.',
color: module.color, style: Theme.of(context).textTheme.bodySmall?.copyWith(
label: module.label, color: Theme.of(context).colorScheme.onSurfaceVariant,
value: _viewPermissions[module.key] ?? false, ),
onChanged: (v) => )
setState(() => _viewPermissions[module.key] = v), 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),
);
}),
], ],
], ],
), ),

View File

@ -57,7 +57,8 @@ class RoleRemoteDataSource {
} }
Future<RoleModel> createRole(CreateRoleRequest request) async { Future<RoleModel> 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); return _parseRole(response.data);
} }

View File

@ -10,6 +10,13 @@ const permissionMatrixActionOrder = [
'export', 'export',
]; ];
class ModulePermissionEntry {
const ModulePermissionEntry({required this.id, required this.action});
final String id;
final String action;
}
String permissionActionLabel(String action) => switch (action) { String permissionActionLabel(String action) => switch (action) {
'view' => 'VIEW', 'view' => 'VIEW',
'create' => 'CREATE', 'create' => 'CREATE',
@ -32,25 +39,47 @@ class PermissionModuleCatalog {
required this.code, required this.code,
required this.name, required this.name,
required this.actions, required this.actions,
required this.permissions,
}); });
final String id; final String id;
final String code; final String code;
final String name; final String name;
final List<String> actions; final List<String> actions;
final List<ModulePermissionEntry> 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<String, dynamic> json) { factory PermissionModuleCatalog.fromJson(Map<String, dynamic> json) {
final permissions = json['permissions'] as List<dynamic>? ?? const []; final permissions = <ModulePermissionEntry>[];
final actions = permissions for (final item in json['permissions'] as List<dynamic>? ?? const []) {
.map((item) => (item as Map<String, dynamic>)['action'] as String?) if (item is! Map<String, dynamic>) continue;
.whereType<String>() final action = item['action'] as String?;
.toList(); if (action == null) continue;
permissions.add(
ModulePermissionEntry(
id: item['id']?.toString() ?? '',
action: action,
),
);
}
final actions = permissions.map((entry) => entry.action).toList();
return PermissionModuleCatalog( return PermissionModuleCatalog(
id: json['id']?.toString() ?? '', id: json['id']?.toString() ?? '',
code: json['code'] as String? ?? '', code: json['code'] as String? ?? '',
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
actions: sortPermissionActions(actions), actions: sortPermissionActions(actions),
permissions: permissions,
); );
} }
} }
@ -182,9 +211,9 @@ class RolePermissionMatrix {
'ROLES' => styles[1], 'ROLES' => styles[1],
'MASTERS' => styles[2], 'MASTERS' => styles[2],
'VENDOR' => styles[3], 'VENDOR' => styles[3],
'PURCHASE_ORDERS' || 'PO' => styles[4], 'PURCHASE_ORDERS' || 'PURCHASE_ORDER' || 'PO' => styles[4],
'GRN' => styles[5], 'GRN' => styles[5],
'ASSETS' => styles[6], 'ASSETS' || 'ASSET' || 'ASSET_MANAGEMENT' => styles[6],
_ => styles[index % styles.length], _ => styles[index % styles.length],
}; };
return byCode; return byCode;

View File

@ -231,6 +231,8 @@ class CreateRoleRequest with _$CreateRoleRequest {
required String name, required String name,
String? description, String? description,
@JsonKey(name: 'is_active') @Default(true) bool isActive, @JsonKey(name: 'is_active') @Default(true) bool isActive,
@JsonKey(name: 'permission_ids') @Default([]) List<int> permissionIds,
@JsonKey(name: 'view_modules') @Default([]) List<String> viewModules,
}) = _CreateRoleRequest; }) = _CreateRoleRequest;
factory CreateRoleRequest.fromJson(Map<String, dynamic> json) => factory CreateRoleRequest.fromJson(Map<String, dynamic> json) =>

View File

@ -3011,6 +3011,10 @@ mixin _$CreateRoleRequest {
String? get description => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError;
@JsonKey(name: 'is_active') @JsonKey(name: 'is_active')
bool get isActive => throw _privateConstructorUsedError; bool get isActive => throw _privateConstructorUsedError;
@JsonKey(name: 'permission_ids')
List<int> get permissionIds => throw _privateConstructorUsedError;
@JsonKey(name: 'view_modules')
List<String> get viewModules => throw _privateConstructorUsedError;
/// Serializes this CreateRoleRequest to a JSON map. /// Serializes this CreateRoleRequest to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -3033,6 +3037,8 @@ abstract class $CreateRoleRequestCopyWith<$Res> {
String name, String name,
String? description, String? description,
@JsonKey(name: 'is_active') bool isActive, @JsonKey(name: 'is_active') bool isActive,
@JsonKey(name: 'permission_ids') List<int> permissionIds,
@JsonKey(name: 'view_modules') List<String> viewModules,
}); });
} }
@ -3054,6 +3060,8 @@ class _$CreateRoleRequestCopyWithImpl<$Res, $Val extends CreateRoleRequest>
Object? name = null, Object? name = null,
Object? description = freezed, Object? description = freezed,
Object? isActive = null, Object? isActive = null,
Object? permissionIds = null,
Object? viewModules = null,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@ -3069,6 +3077,14 @@ class _$CreateRoleRequestCopyWithImpl<$Res, $Val extends CreateRoleRequest>
? _value.isActive ? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable : isActive // ignore: cast_nullable_to_non_nullable
as bool, as bool,
permissionIds: null == permissionIds
? _value.permissionIds
: permissionIds // ignore: cast_nullable_to_non_nullable
as List<int>,
viewModules: null == viewModules
? _value.viewModules
: viewModules // ignore: cast_nullable_to_non_nullable
as List<String>,
) )
as $Val, as $Val,
); );
@ -3088,6 +3104,8 @@ abstract class _$$CreateRoleRequestImplCopyWith<$Res>
String name, String name,
String? description, String? description,
@JsonKey(name: 'is_active') bool isActive, @JsonKey(name: 'is_active') bool isActive,
@JsonKey(name: 'permission_ids') List<int> permissionIds,
@JsonKey(name: 'view_modules') List<String> viewModules,
}); });
} }
@ -3108,6 +3126,8 @@ class __$$CreateRoleRequestImplCopyWithImpl<$Res>
Object? name = null, Object? name = null,
Object? description = freezed, Object? description = freezed,
Object? isActive = null, Object? isActive = null,
Object? permissionIds = null,
Object? viewModules = null,
}) { }) {
return _then( return _then(
_$CreateRoleRequestImpl( _$CreateRoleRequestImpl(
@ -3123,6 +3143,14 @@ class __$$CreateRoleRequestImplCopyWithImpl<$Res>
? _value.isActive ? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable : isActive // ignore: cast_nullable_to_non_nullable
as bool, as bool,
permissionIds: null == permissionIds
? _value._permissionIds
: permissionIds // ignore: cast_nullable_to_non_nullable
as List<int>,
viewModules: null == viewModules
? _value._viewModules
: viewModules // ignore: cast_nullable_to_non_nullable
as List<String>,
), ),
); );
} }
@ -3135,7 +3163,10 @@ class _$CreateRoleRequestImpl implements _CreateRoleRequest {
required this.name, required this.name,
this.description, this.description,
@JsonKey(name: 'is_active') this.isActive = true, @JsonKey(name: 'is_active') this.isActive = true,
}); @JsonKey(name: 'permission_ids') final List<int> permissionIds = const [],
@JsonKey(name: 'view_modules') final List<String> viewModules = const [],
}) : _permissionIds = permissionIds,
_viewModules = viewModules;
factory _$CreateRoleRequestImpl.fromJson(Map<String, dynamic> json) => factory _$CreateRoleRequestImpl.fromJson(Map<String, dynamic> json) =>
_$$CreateRoleRequestImplFromJson(json); _$$CreateRoleRequestImplFromJson(json);
@ -3147,10 +3178,27 @@ class _$CreateRoleRequestImpl implements _CreateRoleRequest {
@override @override
@JsonKey(name: 'is_active') @JsonKey(name: 'is_active')
final bool isActive; final bool isActive;
final List<int> _permissionIds;
@override
@JsonKey(name: 'permission_ids')
List<int> get permissionIds {
if (_permissionIds is EqualUnmodifiableListView) return _permissionIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_permissionIds);
}
final List<String> _viewModules;
@override
@JsonKey(name: 'view_modules')
List<String> get viewModules {
if (_viewModules is EqualUnmodifiableListView) return _viewModules;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_viewModules);
}
@override @override
String toString() { String toString() {
return 'CreateRoleRequest(name: $name, description: $description, isActive: $isActive)'; return 'CreateRoleRequest(name: $name, description: $description, isActive: $isActive, permissionIds: $permissionIds, viewModules: $viewModules)';
} }
@override @override
@ -3162,12 +3210,27 @@ class _$CreateRoleRequestImpl implements _CreateRoleRequest {
(identical(other.description, description) || (identical(other.description, description) ||
other.description == description) && other.description == description) &&
(identical(other.isActive, isActive) || (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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 /// Create a copy of CreateRoleRequest
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -3191,6 +3254,8 @@ abstract class _CreateRoleRequest implements CreateRoleRequest {
required final String name, required final String name,
final String? description, final String? description,
@JsonKey(name: 'is_active') final bool isActive, @JsonKey(name: 'is_active') final bool isActive,
@JsonKey(name: 'permission_ids') final List<int> permissionIds,
@JsonKey(name: 'view_modules') final List<String> viewModules,
}) = _$CreateRoleRequestImpl; }) = _$CreateRoleRequestImpl;
factory _CreateRoleRequest.fromJson(Map<String, dynamic> json) = factory _CreateRoleRequest.fromJson(Map<String, dynamic> json) =
@ -3203,6 +3268,12 @@ abstract class _CreateRoleRequest implements CreateRoleRequest {
@override @override
@JsonKey(name: 'is_active') @JsonKey(name: 'is_active')
bool get isActive; bool get isActive;
@override
@JsonKey(name: 'permission_ids')
List<int> get permissionIds;
@override
@JsonKey(name: 'view_modules')
List<String> get viewModules;
/// Create a copy of CreateRoleRequest /// Create a copy of CreateRoleRequest
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@ -228,6 +228,16 @@ _$CreateRoleRequestImpl _$$CreateRoleRequestImplFromJson(
name: json['name'] as String, name: json['name'] as String,
description: json['description'] as String?, description: json['description'] as String?,
isActive: json['is_active'] as bool? ?? true, isActive: json['is_active'] as bool? ?? true,
permissionIds:
(json['permission_ids'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList() ??
const [],
viewModules:
(json['view_modules'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
); );
Map<String, dynamic> _$$CreateRoleRequestImplToJson( Map<String, dynamic> _$$CreateRoleRequestImplToJson(
@ -236,6 +246,8 @@ Map<String, dynamic> _$$CreateRoleRequestImplToJson(
'name': instance.name, 'name': instance.name,
'description': instance.description, 'description': instance.description,
'is_active': instance.isActive, 'is_active': instance.isActive,
'permission_ids': instance.permissionIds,
'view_modules': instance.viewModules,
}; };
_$UpdateRoleRequestImpl _$$UpdateRoleRequestImplFromJson( _$UpdateRoleRequestImpl _$$UpdateRoleRequestImplFromJson(

View File

@ -118,11 +118,13 @@ final routerProvider = Provider<GoRouter>((ref) {
routes: [ routes: [
GoRoute( GoRoute(
path: RouteConstants.dashboard, path: RouteConstants.dashboard,
builder: (context, state) => const DashboardScreen(), pageBuilder: (context, state) =>
shellPage(state, const DashboardScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.companies, path: RouteConstants.companies,
builder: (context, state) => const CompanyListScreen(), pageBuilder: (context, state) =>
shellPage(state, const CompanyListScreen()),
routes: [ routes: [
GoRoute( GoRoute(
path: 'add', path: 'add',
@ -137,7 +139,8 @@ final routerProvider = Provider<GoRouter>((ref) {
), ),
GoRoute( GoRoute(
path: RouteConstants.branches, path: RouteConstants.branches,
builder: (context, state) => const BranchListScreen(), pageBuilder: (context, state) =>
shellPage(state, const BranchListScreen()),
routes: [ routes: [
GoRoute( GoRoute(
path: 'add', path: 'add',
@ -152,14 +155,18 @@ final routerProvider = Provider<GoRouter>((ref) {
), ),
GoRoute( GoRoute(
path: RouteConstants.usersRoleManagement, path: RouteConstants.usersRoleManagement,
builder: (context, state) => UsersRoleManagementScreen( pageBuilder: (context, state) => shellPage(
initialTab: rbacTabFromLocation(state.uri.toString()), state,
UsersRoleManagementScreen(
initialTab: rbacTabFromLocation(state.uri.toString()),
),
), ),
), ),
GoRoute( GoRoute(
path: RouteConstants.users, path: RouteConstants.users,
builder: (context, state) => const UsersRoleManagementScreen( pageBuilder: (context, state) => shellPage(
initialTab: RbacTab.users, state,
const UsersRoleManagementScreen(initialTab: RbacTab.users),
), ),
routes: [ routes: [
GoRoute( GoRoute(
@ -180,8 +187,9 @@ final routerProvider = Provider<GoRouter>((ref) {
), ),
GoRoute( GoRoute(
path: RouteConstants.roles, path: RouteConstants.roles,
builder: (context, state) => const UsersRoleManagementScreen( pageBuilder: (context, state) => shellPage(
initialTab: RbacTab.roles, state,
const UsersRoleManagementScreen(initialTab: RbacTab.roles),
), ),
routes: [ routes: [
GoRoute( GoRoute(
@ -194,11 +202,13 @@ final routerProvider = Provider<GoRouter>((ref) {
), ),
GoRoute( GoRoute(
path: RouteConstants.profile, path: RouteConstants.profile,
builder: (context, state) => const UserProfileScreen(), pageBuilder: (context, state) =>
shellPage(state, const UserProfileScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.assets, path: RouteConstants.assets,
builder: (context, state) => const AssetListScreen(), pageBuilder: (context, state) =>
shellPage(state, const AssetListScreen()),
routes: [ routes: [
GoRoute( GoRoute(
path: 'add', path: 'add',
@ -242,27 +252,33 @@ final routerProvider = Provider<GoRouter>((ref) {
), ),
GoRoute( GoRoute(
path: RouteConstants.departments, path: RouteConstants.departments,
builder: (context, state) => const DepartmentsScreen(), pageBuilder: (context, state) =>
shellPage(state, const DepartmentsScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.locations, path: RouteConstants.locations,
builder: (context, state) => const LocationsScreen(), pageBuilder: (context, state) =>
shellPage(state, const LocationsScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.uom, path: RouteConstants.uom,
builder: (context, state) => const UomScreen(), pageBuilder: (context, state) =>
shellPage(state, const UomScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.reports, path: RouteConstants.reports,
builder: (context, state) => const ReportsScreen(), pageBuilder: (context, state) =>
shellPage(state, const ReportsScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.auditLogs, path: RouteConstants.auditLogs,
builder: (context, state) => const AuditLogsScreen(), pageBuilder: (context, state) =>
shellPage(state, const AuditLogsScreen()),
), ),
GoRoute( GoRoute(
path: RouteConstants.settings, path: RouteConstants.settings,
builder: (context, state) => const SettingsScreen(), pageBuilder: (context, state) =>
shellPage(state, const SettingsScreen()),
routes: [ routes: [
GoRoute( GoRoute(
path: 'general', path: 'general',
@ -301,7 +317,8 @@ final routerProvider = Provider<GoRouter>((ref) {
if (DevConfig.screenPreviewEnabled) if (DevConfig.screenPreviewEnabled)
GoRoute( GoRoute(
path: RouteConstants.screenGallery, 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<void> shellPage(GoRouterState state, Widget child) {
return NoTransitionPage<void>(
key: state.pageKey,
child: child,
);
}
class _AuthListenable extends ChangeNotifier { class _AuthListenable extends ChangeNotifier {
_AuthListenable(this._ref) { _AuthListenable(this._ref) {
_ref.listen(authStateProvider, (_, __) => notifyListeners()); _ref.listen(authStateProvider, (_, __) => notifyListeners());

View File

@ -30,6 +30,20 @@ class _AppShellState extends ConsumerState<AppShell> {
final _scaffoldKey = GlobalKey<ScaffoldState>(); final _scaffoldKey = GlobalKey<ScaffoldState>();
bool _sidebarCollapsed = false; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final authState = ref.watch(authStateProvider); final authState = ref.watch(authStateProvider);
@ -71,10 +85,7 @@ class _AppShellState extends ConsumerState<AppShell> {
_scaffoldKey.currentState?.closeDrawer(); _scaffoldKey.currentState?.closeDrawer();
}, },
), ),
body: ThemeKeyedSubtree( body: _pageContent(context, currentRoute, widget.child),
pageKey: currentRoute,
child: widget.child,
),
); );
} }
@ -97,10 +108,7 @@ class _AppShellState extends ConsumerState<AppShell> {
onItemTap: (route) => goAndDismissOverlays(context, route), onItemTap: (route) => goAndDismissOverlays(context, route),
), ),
Expanded( Expanded(
child: ThemeKeyedSubtree( child: _pageContent(context, currentRoute, widget.child),
pageKey: currentRoute,
child: widget.child,
),
), ),
], ],
), ),
@ -126,10 +134,7 @@ class _AppShellState extends ConsumerState<AppShell> {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: ThemeKeyedSubtree( child: _pageContent(context, currentRoute, widget.child),
pageKey: currentRoute,
child: widget.child,
),
), ),
], ],
), ),