bharat_erp/lib/modules/rbac/presentation/widgets/add_user_panel.dart

428 lines
16 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/user_management_models.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_dropdown.dart';
import '../../../../shared/widgets/app_loading_view.dart';
import '../../../../shared/widgets/app_searchable_dropdown.dart';
import '../../../../shared/widgets/app_searchable_multi_select_dropdown.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/error_view.dart';
import '../../../../shared/widgets/app_side_panel.dart';
import '../../../master_data/presentation/widgets/master_quick_add.dart';
import '../providers/add_user_form_provider.dart';
import '../../../../shared/widgets/app_toast.dart';
class AddUserPanel extends ConsumerStatefulWidget {
const AddUserPanel({super.key, this.userId});
final String? userId;
bool get isEditing => userId != null;
@override
ConsumerState<AddUserPanel> createState() => _AddUserPanelState();
}
class _AddUserPanelState extends ConsumerState<AddUserPanel> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _employeeCodeController = TextEditingController();
final _emailController = TextEditingController();
final _mobileController = TextEditingController();
final _passwordController = TextEditingController();
List<String> _selectedRoleIds = [];
String _selectedStatus = 'Active';
String? _selectedDepartmentId;
String? _selectedPlantId;
String? _selectedDesignationId;
String? _selectedReportingToId;
bool _prefilled = false;
bool _obscurePassword = true;
static const _statusOptions = [
AppDropdownOption(value: 'Active', label: 'Active'),
AppDropdownOption(value: 'Inactive', label: 'Inactive'),
AppDropdownOption(value: 'Locked', label: 'Locked'),
];
@override
void dispose() {
_nameController.dispose();
_employeeCodeController.dispose();
_emailController.dispose();
_mobileController.dispose();
_passwordController.dispose();
super.dispose();
}
String _statusToApi(String label) => switch (label) {
'Active' => 'active',
'Inactive' => 'inactive',
'Locked' => 'locked',
_ => 'active',
};
String _statusFromApi(String status) => switch (status.toLowerCase()) {
'active' => 'Active',
'inactive' => 'Inactive',
'locked' => 'Locked',
_ => 'Active',
};
void _prefillFromUser(ManagedUserModel user) {
if (_prefilled) return;
_prefilled = true;
_nameController.text = user.fullName;
_employeeCodeController.text = user.employeeCode;
_emailController.text = user.email;
_mobileController.text = user.mobile;
_selectedRoleIds = user.effectiveRoleIds;
_selectedDepartmentId = user.departmentId;
_selectedDesignationId = user.designationId;
_selectedPlantId = user.plantId;
_selectedReportingToId = user.reportingTo;
_selectedStatus = _statusLabelFromUser(user);
}
String _statusLabelFromUser(ManagedUserModel user) {
final normalized = user.status.trim().toLowerCase();
if (normalized.isNotEmpty && normalized != 'active') {
return _statusFromApi(user.status);
}
if (!user.isActive) return 'Inactive';
return _statusFromApi(user.status);
}
List<AppDropdownOption<String>> _toOptions(List<FilterOptionModel> items) {
return items
.map((item) => AppDropdownOption(value: item.id, label: item.name))
.toList();
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
final roleIds = _selectedRoleIds
.map((id) => int.tryParse(id))
.whereType<int>()
.toList();
if (roleIds.isEmpty) {
showAppToastFromSnackBar(context,
const SnackBar(content: Text('Please select at least one role')),
);
return;
}
final notifier = ref.read(addUserFormProvider(widget.userId).notifier);
final bool success;
if (widget.isEditing) {
success = await notifier.submitUpdate(
widget.userId!,
UpdateUserRequest(
employeeCode: _employeeCodeController.text.trim(),
fullName: _nameController.text.trim(),
email: _emailController.text.trim(),
password: _passwordController.text.isEmpty ? null : _passwordController.text,
mobile: _mobileController.text.trim().isEmpty
? null
: _mobileController.text.trim(),
roleId: roleIds.first,
roleIds: roleIds,
departmentId: int.tryParse(_selectedDepartmentId ?? ''),
designationId: int.tryParse(_selectedDesignationId ?? ''),
plantId: int.tryParse(_selectedPlantId ?? ''),
reportingTo: int.tryParse(_selectedReportingToId ?? ''),
status: _statusToApi(_selectedStatus),
isActive: _selectedStatus == 'Active',
),
);
} else {
success = await notifier.submitCreate(
CreateUserRequest(
employeeCode: _employeeCodeController.text.trim(),
fullName: _nameController.text.trim(),
email: _emailController.text.trim(),
password: _passwordController.text,
mobile: _mobileController.text.trim().isEmpty
? null
: _mobileController.text.trim(),
roleId: roleIds.first,
roleIds: roleIds,
departmentId: int.tryParse(_selectedDepartmentId ?? ''),
designationId: int.tryParse(_selectedDesignationId ?? ''),
plantId: int.tryParse(_selectedPlantId ?? ''),
reportingTo: int.tryParse(_selectedReportingToId ?? ''),
status: _statusToApi(_selectedStatus),
isActive: _selectedStatus == 'Active',
),
);
}
if (!mounted) return;
if (success) {
Navigator.of(context, rootNavigator: true).pop(true);
return;
}
final error = ref.read(addUserFormProvider(widget.userId)).valueOrNull?.errorMessage;
showAppToastFromSnackBar(context,
SnackBar(
content: Text(
error ?? (widget.isEditing ? 'Failed to update user' : 'Failed to create user'),
),
),
);
}
Widget _buildDropdown({
required String label,
required String? value,
required List<FilterOptionModel> options,
required ValueChanged<String?> onChanged,
String? hint,
bool required = false,
String? masterId,
Map<String, dynamic>? initialValues,
}) {
final fieldLabel = required ? '$label *' : label;
final fieldHint = hint ?? 'Select ${label.toLowerCase()}';
final fieldEnabled = options.isNotEmpty || masterId != null;
final validator =
required ? (String? v) => v == null ? 'Please select $label' : null : null;
if (masterId == null) {
return AppSearchableDropdown<String>(
label: fieldLabel,
value: value,
hint: fieldHint,
searchHint: 'Search $label...',
enabled: fieldEnabled,
options: _toOptions(options),
onChanged: onChanged,
validator: validator,
);
}
return MasterQuickAddDropdown<String>(
masterId: masterId,
label: fieldLabel,
value: value,
hint: fieldHint,
searchHint: 'Search $label...',
enabled: fieldEnabled,
options: _toOptions(options),
initialValues: initialValues,
refreshLookups: () =>
ref.invalidate(addUserFormProvider(widget.userId)),
parseCreatedId: (id) => id,
onChanged: onChanged,
validator: validator,
);
}
@override
Widget build(BuildContext context) {
final formAsync = ref.watch(addUserFormProvider(widget.userId));
final isSubmitting = formAsync.valueOrNull?.isSubmitting ?? false;
return SidePanelScaffold(
title: widget.isEditing ? 'Edit User' : 'Add User',
footer: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: isSubmitting
? null
: () => Navigator.of(context, rootNavigator: true).pop(),
child: const Text('Cancel'),
),
const SizedBox(width: 12),
AppButton(
label: widget.isEditing ? 'Update User' : 'Save User',
expand: false,
icon: Icons.check,
isLoading: isSubmitting,
onPressed: isSubmitting ? null : _save,
),
],
),
child: formAsync.when(
loading: () => AppLoadingView(
message: widget.isEditing ? 'Loading user...' : 'Loading form options...',
),
error: (error, _) => ErrorView.fromFailure(
error is Failure ? error : Failure.unknown(message: error.toString()),
onRetry: () => ref.invalidate(addUserFormProvider(widget.userId)),
),
data: (formState) {
if (formState.editingUser != null) {
_prefillFromUser(formState.editingUser!);
}
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SidePanelFormRow(
left: AppTextField(
controller: _nameController,
label: 'Full Name *',
hint: 'e.g. Ravi Kumar',
validator: (v) => Validators.required(v, fieldName: 'Name'),
),
right: AppTextField(
controller: _employeeCodeController,
label: 'Employee Code *',
hint: 'e.g. EMP002',
validator: (v) =>
Validators.required(v, fieldName: 'Employee Code'),
),
),
SidePanelFormRow(
left: AppTextField(
controller: _emailController,
label: 'Email *',
hint: 'ravi@company.com',
keyboardType: TextInputType.emailAddress,
validator: Validators.email,
),
right: AppTextField(
controller: _mobileController,
label: 'Mobile',
hint: '9XXXXXXXXX',
keyboardType: TextInputType.phone,
validator: Validators.optionalMobile,
inputFormatters: Validators.mobileInput,
),
),
const SizedBox(height: 12),
SidePanelSection(
title: 'ROLE & ACCESS',
children: [
SidePanelFormRow(
left: AppSearchableMultiSelectDropdown<String>(
label: 'Role *',
values: _selectedRoleIds,
hint: formState.roles.isEmpty
? 'No roles available'
: 'Select roles',
searchHint: 'Search role...',
enabled: formState.roles.isNotEmpty,
options: _toOptions(formState.roles),
onChanged: (ids) =>
setState(() => _selectedRoleIds = ids),
validator: (ids) =>
ids == null || ids.isEmpty
? 'Please select at least one role'
: null,
),
right: AppSearchableDropdown<String>(
label: 'Status',
value: _selectedStatus,
options: _statusOptions,
searchHint: 'Search status...',
onChanged: (v) =>
setState(() => _selectedStatus = v ?? 'Active'),
),
),
],
),
SidePanelSection(
title: 'ORGANISATION',
children: [
SidePanelFormRow(
left: _buildDropdown(
label: 'Department',
value: _selectedDepartmentId,
options: formState.departments,
masterId: 'departments',
onChanged: (v) =>
setState(() => _selectedDepartmentId = v),
),
right: _buildDropdown(
label: 'Designation',
value: _selectedDesignationId,
options: formState.designations,
masterId: 'designations',
onChanged: (v) =>
setState(() => _selectedDesignationId = v),
),
),
SidePanelFormRow(
left: _buildDropdown(
label: 'Plant / Unit',
value: _selectedPlantId,
options: formState.plants,
masterId: 'locations',
initialValues: const {'type': 'plant'},
onChanged: (v) => setState(() => _selectedPlantId = v),
),
right: _buildDropdown(
label: 'Reporting To',
value: _selectedReportingToId,
options: formState.managers,
hint: formState.managers.isEmpty
? 'No managers available'
: 'Select manager',
onChanged: (v) =>
setState(() => _selectedReportingToId = v),
),
),
],
),
SidePanelSection(
title: 'PASSWORD',
children: [
AppTextField(
controller: _passwordController,
label: widget.isEditing
? 'New Password'
: 'Temporary Password *',
hint: '8+ chars, upper, lower, digit, special',
obscureText: _obscurePassword,
suffixIcon: IconButton(
tooltip: _obscurePassword
? 'Show password'
: 'Hide password',
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
size: 20,
),
onPressed: () => setState(
() => _obscurePassword = !_obscurePassword,
),
),
validator: widget.isEditing
? Validators.optionalPassword
: Validators.password,
),
const SizedBox(height: 4),
Text(
widget.isEditing
? 'Leave blank to keep the current password.'
: 'User will be asked to change this on first login.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
);
},
),
);
}
}