224 lines
7.7 KiB
Dart
224 lines
7.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
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 '../../../../shared/widgets/app_side_panel.dart';
|
|
import '../providers/role_form_provider.dart';
|
|
import 'rbac_widgets.dart';
|
|
import '../../../../shared/widgets/app_toast.dart';
|
|
|
|
class RoleFormPanel extends ConsumerStatefulWidget {
|
|
const RoleFormPanel({super.key, this.roleId});
|
|
|
|
final String? roleId;
|
|
|
|
bool get isEditing => roleId != null;
|
|
|
|
@override
|
|
ConsumerState<RoleFormPanel> createState() => _RoleFormPanelState();
|
|
}
|
|
|
|
class _RoleFormPanelState extends ConsumerState<RoleFormPanel> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _nameController = TextEditingController();
|
|
final _descriptionController = TextEditingController();
|
|
final Map<String, bool> _viewPermissions = {};
|
|
bool _prefilled = false;
|
|
bool _permissionsInitialized = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_descriptionController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _prefillFromRole(RoleCardModel role) {
|
|
if (_prefilled) return;
|
|
_prefilled = true;
|
|
_nameController.text = role.name;
|
|
_descriptionController.text = role.description ?? '';
|
|
}
|
|
|
|
Future<void> _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;
|
|
|
|
if (widget.isEditing) {
|
|
success = await notifier.submitUpdate(
|
|
widget.roleId!,
|
|
UpdateRoleRequest(
|
|
name: _nameController.text.trim(),
|
|
description: _descriptionController.text.trim(),
|
|
),
|
|
);
|
|
} else {
|
|
success = await notifier.submitCreate(
|
|
buildCreateRoleRequest(
|
|
name: _nameController.text.trim(),
|
|
description: _descriptionController.text.trim(),
|
|
selectedModules: _viewPermissions,
|
|
catalog: formState.permissionCatalog,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
if (success) {
|
|
Navigator.of(context, rootNavigator: true).pop(true);
|
|
return;
|
|
}
|
|
|
|
final error = ref.read(roleFormProvider(widget.roleId)).valueOrNull?.errorMessage;
|
|
showAppToastFromSnackBar(context,
|
|
SnackBar(
|
|
content: Text(
|
|
error ??
|
|
(widget.isEditing ? 'Failed to update role' : 'Failed to create role'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final formAsync = ref.watch(roleFormProvider(widget.roleId));
|
|
final isSubmitting = formAsync.valueOrNull?.isSubmitting ?? false;
|
|
|
|
return SidePanelScaffold(
|
|
title: widget.isEditing ? 'Edit Role' : 'Create New Role',
|
|
footer: Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: isSubmitting
|
|
? null
|
|
: () => Navigator.of(context, rootNavigator: true).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: AppButton(
|
|
label: widget.isEditing ? 'Update Role' : 'Save Role',
|
|
expand: true,
|
|
isLoading: isSubmitting,
|
|
onPressed: isSubmitting ? null : _save,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
child: formAsync.when(
|
|
loading: () => AppLoadingView(
|
|
message: widget.isEditing ? 'Loading role...' : 'Loading form...',
|
|
),
|
|
error: (error, _) => ErrorView.fromFailure(
|
|
error is Failure ? error : Failure.unknown(message: error.toString()),
|
|
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;
|
|
setState(() => _prefillFromRole(formState.editingRole!));
|
|
});
|
|
}
|
|
|
|
return Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
AppTextField(
|
|
controller: _nameController,
|
|
label: 'Role Name *',
|
|
hint: 'e.g. QC Manager',
|
|
validator: Validators.roleName,
|
|
inputFormatters: Validators.roleNameInput,
|
|
),
|
|
const SizedBox(height: 16),
|
|
AppTextField(
|
|
controller: _descriptionController,
|
|
label: 'Description',
|
|
hint: 'What does this role do?',
|
|
maxLines: 3,
|
|
),
|
|
if (!widget.isEditing) ...[
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'INITIAL PERMISSIONS',
|
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.8,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'You can fine-tune permissions in the Permission Matrix tab after creating the role.',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
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),
|
|
);
|
|
}),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Backwards-compatible alias.
|
|
typedef CreateRolePanel = RoleFormPanel;
|