feat(user): 사용자 자기정보 편집과 관리자 재설정 플로우를 연동
- lib/widgets/app_shell.dart에서 내 정보 다이얼로그를 추가하고 UserRepository.updateMe·비밀번호 변경 로직을 연결 - lib/features/masters/user/* 모듈에 phone·forcePasswordChange·passwordUpdatedAt 필드를 반영하고 reset-password/update-me API를 사용 - lib/core/validation/password_rules.dart을 신설해 비밀번호 정책 검증을 공통화하고 신규 위젯·테스트에서 재사용 - doc/stock_approval_system_api_v4.md 등 문서를 users 스펙 개편 내용으로 갱신하고 user_management_plan.md를 추가 - test/widgets/app_shell_test.dart 등에서 자기정보 수정·비밀번호 재설정 시나리오를 검증하고 기존 테스트를 보강
This commit is contained in:
@@ -15,6 +15,8 @@ class UserDto {
|
||||
this.isActive = true,
|
||||
this.isDeleted = false,
|
||||
this.note,
|
||||
this.forcePasswordChange = false,
|
||||
this.passwordUpdatedAt,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
@@ -28,6 +30,8 @@ class UserDto {
|
||||
final bool isActive;
|
||||
final bool isDeleted;
|
||||
final String? note;
|
||||
final bool forcePasswordChange;
|
||||
final DateTime? passwordUpdatedAt;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@@ -35,16 +39,22 @@ class UserDto {
|
||||
factory UserDto.fromJson(Map<String, dynamic> json) {
|
||||
return UserDto(
|
||||
id: json['id'] as int?,
|
||||
employeeNo: json['employee_no'] as String,
|
||||
employeeName: json['employee_name'] as String,
|
||||
employeeNo:
|
||||
json['employee_id'] as String? ??
|
||||
json['employee_no'] as String? ??
|
||||
'-',
|
||||
employeeName:
|
||||
json['name'] as String? ?? json['employee_name'] as String? ?? '-',
|
||||
email: json['email'] as String?,
|
||||
mobileNo: json['mobile_no'] as String?,
|
||||
mobileNo: json['phone'] as String? ?? json['mobile_no'] as String?,
|
||||
group: json['group'] is Map<String, dynamic>
|
||||
? UserGroupDto.fromJson(json['group'] as Map<String, dynamic>)
|
||||
: null,
|
||||
isActive: (json['is_active'] as bool?) ?? true,
|
||||
isDeleted: (json['is_deleted'] as bool?) ?? false,
|
||||
note: json['note'] as String?,
|
||||
forcePasswordChange: (json['force_password_change'] as bool?) ?? false,
|
||||
passwordUpdatedAt: _parseDate(json['password_updated_at']),
|
||||
createdAt: _parseDate(json['created_at']),
|
||||
updatedAt: _parseDate(json['updated_at']),
|
||||
);
|
||||
@@ -61,6 +71,8 @@ class UserDto {
|
||||
isActive: isActive,
|
||||
isDeleted: isDeleted,
|
||||
note: note,
|
||||
forcePasswordChange: forcePasswordChange,
|
||||
passwordUpdatedAt: passwordUpdatedAt,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ class UserRepositoryRemote implements UserRepository {
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '${ApiRoutes.apiV1}/employees';
|
||||
static const _basePath = '${ApiRoutes.apiV1}/users';
|
||||
|
||||
/// 사용자 목록을 조회한다.
|
||||
@override
|
||||
@@ -31,7 +31,7 @@ class UserRepositoryRemote implements UserRepository {
|
||||
'page_size': pageSize,
|
||||
if (query != null && query.isNotEmpty) 'q': query,
|
||||
if (groupId != null) 'group_id': groupId,
|
||||
if (isActive != null) 'active': isActive,
|
||||
if (isActive != null) 'is_active': isActive,
|
||||
'include': 'group',
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
@@ -62,6 +62,27 @@ class UserRepositoryRemote implements UserRepository {
|
||||
return UserDto.fromJson(_api.unwrapAsMap(response)).toEntity();
|
||||
}
|
||||
|
||||
/// 로그인한 사용자의 정보를 수정한다.
|
||||
@override
|
||||
Future<UserAccount> updateMe(UserProfileUpdateInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
'$_basePath/me',
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return UserDto.fromJson(_api.unwrapAsMap(response)).toEntity();
|
||||
}
|
||||
|
||||
/// 관리자가 특정 사용자의 비밀번호를 재설정한다.
|
||||
@override
|
||||
Future<UserAccount> resetPassword(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/reset-password',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return UserDto.fromJson(_api.unwrapAsMap(response)).toEntity();
|
||||
}
|
||||
|
||||
/// 사용자를 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
|
||||
@@ -10,6 +10,8 @@ class UserAccount {
|
||||
this.isActive = true,
|
||||
this.isDeleted = false,
|
||||
this.note,
|
||||
this.forcePasswordChange = false,
|
||||
this.passwordUpdatedAt,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
@@ -23,6 +25,8 @@ class UserAccount {
|
||||
final bool isActive;
|
||||
final bool isDeleted;
|
||||
final String? note;
|
||||
final bool forcePasswordChange;
|
||||
final DateTime? passwordUpdatedAt;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@@ -37,6 +41,8 @@ class UserAccount {
|
||||
bool? isActive,
|
||||
bool? isDeleted,
|
||||
String? note,
|
||||
bool? forcePasswordChange,
|
||||
DateTime? passwordUpdatedAt,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
@@ -50,6 +56,8 @@ class UserAccount {
|
||||
isActive: isActive ?? this.isActive,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
note: note ?? this.note,
|
||||
forcePasswordChange: forcePasswordChange ?? this.forcePasswordChange,
|
||||
passwordUpdatedAt: passwordUpdatedAt ?? this.passwordUpdatedAt,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
@@ -73,6 +81,8 @@ class UserInput {
|
||||
this.email,
|
||||
this.mobileNo,
|
||||
this.isActive = true,
|
||||
this.forcePasswordChange,
|
||||
this.password,
|
||||
this.note,
|
||||
});
|
||||
|
||||
@@ -82,18 +92,48 @@ class UserInput {
|
||||
final String? email;
|
||||
final String? mobileNo;
|
||||
final bool isActive;
|
||||
final bool? forcePasswordChange;
|
||||
final String? password;
|
||||
final String? note;
|
||||
|
||||
/// API 요청 바디로 직렬화한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'employee_no': employeeNo,
|
||||
'employee_name': employeeName,
|
||||
'employee_id': employeeNo,
|
||||
'name': employeeName,
|
||||
'group_id': groupId,
|
||||
'email': email,
|
||||
'mobile_no': mobileNo,
|
||||
if (email != null) 'email': email,
|
||||
if (mobileNo != null) 'phone': mobileNo,
|
||||
'is_active': isActive,
|
||||
'note': note,
|
||||
if (forcePasswordChange != null)
|
||||
'force_password_change': forcePasswordChange,
|
||||
if (password != null) 'password': password,
|
||||
if (note != null) 'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 자기 정보 수정 입력 모델.
|
||||
class UserProfileUpdateInput {
|
||||
const UserProfileUpdateInput({
|
||||
this.email,
|
||||
this.phone,
|
||||
this.password,
|
||||
this.currentPassword,
|
||||
});
|
||||
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String? password;
|
||||
final String? currentPassword;
|
||||
|
||||
/// 자기 정보 수정 요청 바디를 직렬화한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
if (email != null) 'email': email,
|
||||
if (phone != null) 'phone': phone,
|
||||
if (password != null) 'password': password,
|
||||
if (currentPassword != null) 'current_password': currentPassword,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ abstract class UserRepository {
|
||||
/// 사용자 정보를 수정한다.
|
||||
Future<UserAccount> update(int id, UserInput input);
|
||||
|
||||
/// 로그인한 본인 정보를 수정한다.
|
||||
Future<UserAccount> updateMe(UserProfileUpdateInput input);
|
||||
|
||||
/// 특정 사용자의 비밀번호를 재설정한다.
|
||||
Future<UserAccount> resetPassword(int id);
|
||||
|
||||
/// 사용자를 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
|
||||
@@ -163,6 +163,23 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 관리자가 사용자의 비밀번호를 재설정한다.
|
||||
Future<UserAccount?> resetPassword(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
final updated = await _userRepository.resetPassword(id);
|
||||
await fetch(page: _result?.page ?? 1);
|
||||
return updated;
|
||||
} catch (error) {
|
||||
final failure = Failure.from(error);
|
||||
_errorMessage = failure.describe();
|
||||
notifyListeners();
|
||||
return null;
|
||||
} finally {
|
||||
_setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자를 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:superport_v2/widgets/components/superport_pagination_controls.da
|
||||
|
||||
import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../core/permissions/permission_manager.dart';
|
||||
import '../../../../../core/validation/password_rules.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../../../group/domain/entities/group.dart';
|
||||
import '../../../group/domain/repositories/group_repository.dart';
|
||||
@@ -318,6 +319,9 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
: (user) => _openUserForm(context, user: user),
|
||||
onDelete: _controller.isSubmitting ? null : _confirmDelete,
|
||||
onRestore: _controller.isSubmitting ? null : _restoreUser,
|
||||
onResetPassword: _controller.isSubmitting
|
||||
? null
|
||||
: _confirmResetPassword,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -367,13 +371,17 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
final mobileController = TextEditingController(
|
||||
text: existing?.mobileNo ?? '',
|
||||
);
|
||||
final passwordController = TextEditingController();
|
||||
final noteController = TextEditingController(text: existing?.note ?? '');
|
||||
final groupNotifier = ValueNotifier<int?>(existing?.group?.id);
|
||||
final isActiveNotifier = ValueNotifier<bool>(existing?.isActive ?? true);
|
||||
final saving = ValueNotifier<bool>(false);
|
||||
final codeError = ValueNotifier<String?>(null);
|
||||
final nameError = ValueNotifier<String?>(null);
|
||||
final emailError = ValueNotifier<String?>(null);
|
||||
final phoneError = ValueNotifier<String?>(null);
|
||||
final groupError = ValueNotifier<String?>(null);
|
||||
final passwordError = ValueNotifier<String?>(null);
|
||||
|
||||
if (groupNotifier.value == null && _controller.groups.length == 1) {
|
||||
groupNotifier.value = _controller.groups.first.id;
|
||||
@@ -398,13 +406,30 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
final note = noteController.text.trim();
|
||||
final groupId = groupNotifier.value;
|
||||
|
||||
if (!isEdit) {
|
||||
final password = passwordController.text;
|
||||
if (password.isEmpty) {
|
||||
passwordError.value = '임시 비밀번호를 입력하세요.';
|
||||
} else {
|
||||
final violations = PasswordRules.validate(password);
|
||||
passwordError.value = violations.isEmpty
|
||||
? null
|
||||
: _describePasswordViolations(violations);
|
||||
}
|
||||
}
|
||||
|
||||
codeError.value = code.isEmpty ? '사번을 입력하세요.' : null;
|
||||
nameError.value = name.isEmpty ? '성명을 입력하세요.' : null;
|
||||
emailError.value = email.isEmpty ? '이메일을 입력하세요.' : null;
|
||||
phoneError.value = mobile.isEmpty ? '연락처를 입력하세요.' : null;
|
||||
groupError.value = groupId == null ? '그룹을 선택하세요.' : null;
|
||||
|
||||
if (codeError.value != null ||
|
||||
nameError.value != null ||
|
||||
groupError.value != null) {
|
||||
emailError.value != null ||
|
||||
phoneError.value != null ||
|
||||
groupError.value != null ||
|
||||
(!isEdit && passwordError.value != null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -420,6 +445,8 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
email: email.isEmpty ? null : email,
|
||||
mobileNo: mobile.isEmpty ? null : mobile,
|
||||
isActive: isActiveNotifier.value,
|
||||
password: isEdit ? null : passwordController.text,
|
||||
forcePasswordChange: isEdit ? null : true,
|
||||
note: note.isEmpty ? null : note,
|
||||
);
|
||||
final response = isEdit
|
||||
@@ -470,6 +497,7 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInput(
|
||||
key: const ValueKey('user_form_employee'),
|
||||
controller: codeController,
|
||||
readOnly: isEdit,
|
||||
onChanged: (_) {
|
||||
@@ -503,6 +531,7 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInput(
|
||||
key: const ValueKey('user_form_name'),
|
||||
controller: nameController,
|
||||
onChanged: (_) {
|
||||
if (nameController.text.trim().isNotEmpty) {
|
||||
@@ -525,21 +554,120 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormField(
|
||||
label: '이메일',
|
||||
child: ShadInput(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
if (!isEdit) ...[
|
||||
const SizedBox(height: 16),
|
||||
ValueListenableBuilder<String?>(
|
||||
valueListenable: passwordError,
|
||||
builder: (_, errorText, __) {
|
||||
return _FormField(
|
||||
label: '임시 비밀번호',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInput(
|
||||
key: const ValueKey('user_form_password'),
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
placeholder: const Text('임시 비밀번호를 입력하세요'),
|
||||
onChanged: (_) {
|
||||
final value = passwordController.text;
|
||||
if (value.isEmpty) {
|
||||
passwordError.value = null;
|
||||
return;
|
||||
}
|
||||
if (PasswordRules.isValid(value)) {
|
||||
passwordError.value = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'비밀번호는 8~24자이며 대문자, 소문자, 숫자, 특수문자를 각각 1자 이상 포함해야 합니다.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
if (errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
errorText,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
ValueListenableBuilder<String?>(
|
||||
valueListenable: emailError,
|
||||
builder: (_, errorText, __) {
|
||||
return _FormField(
|
||||
label: '이메일',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInput(
|
||||
key: const ValueKey('user_form_email'),
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: (_) {
|
||||
if (emailController.text.trim().isNotEmpty) {
|
||||
emailError.value = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
if (errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
errorText,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormField(
|
||||
label: '연락처',
|
||||
child: ShadInput(
|
||||
controller: mobileController,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
ValueListenableBuilder<String?>(
|
||||
valueListenable: phoneError,
|
||||
builder: (_, errorText, __) {
|
||||
return _FormField(
|
||||
label: '연락처',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInput(
|
||||
key: const ValueKey('user_form_phone'),
|
||||
controller: mobileController,
|
||||
keyboardType: TextInputType.phone,
|
||||
onChanged: (_) {
|
||||
if (mobileController.text.trim().isNotEmpty) {
|
||||
phoneError.value = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
if (errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
errorText,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ValueListenableBuilder<int?>(
|
||||
@@ -638,13 +766,17 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
nameController.dispose();
|
||||
emailController.dispose();
|
||||
mobileController.dispose();
|
||||
passwordController.dispose();
|
||||
noteController.dispose();
|
||||
groupNotifier.dispose();
|
||||
isActiveNotifier.dispose();
|
||||
saving.dispose();
|
||||
codeError.dispose();
|
||||
nameError.dispose();
|
||||
emailError.dispose();
|
||||
phoneError.dispose();
|
||||
groupError.dispose();
|
||||
passwordError.dispose();
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(UserAccount user) async {
|
||||
@@ -684,6 +816,47 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmResetPassword(UserAccount user) async {
|
||||
final userId = user.id;
|
||||
if (userId == null) {
|
||||
_showSnack('ID 정보가 없어 비밀번호를 재설정할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
final confirmed = await SuperportDialog.show<bool>(
|
||||
context: context,
|
||||
dialog: SuperportDialog(
|
||||
title: '비밀번호 재설정',
|
||||
description:
|
||||
'"${user.employeeName}" 사용자의 비밀번호를 재설정하고 임시 비밀번호를 이메일로 발송합니다.',
|
||||
actions: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: true).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: true).pop(true),
|
||||
child: const Text('재설정'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final updated = await _controller.resetPassword(userId);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (updated != null) {
|
||||
_showSnack('임시 비밀번호를 이메일로 발송했습니다.');
|
||||
} else if (_controller.errorMessage != null) {
|
||||
_showSnack(_controller.errorMessage!);
|
||||
_controller.clearError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnack(String message) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@@ -695,6 +868,33 @@ class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
messenger.showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
String _describePasswordViolations(List<PasswordRuleViolation> violations) {
|
||||
final messages = <String>[];
|
||||
for (final violation in violations) {
|
||||
switch (violation) {
|
||||
case PasswordRuleViolation.tooShort:
|
||||
messages.add('최소 8자 이상 입력해야 합니다.');
|
||||
break;
|
||||
case PasswordRuleViolation.tooLong:
|
||||
messages.add('최대 24자 이하로 입력해야 합니다.');
|
||||
break;
|
||||
case PasswordRuleViolation.missingUppercase:
|
||||
messages.add('대문자를 최소 1자 포함해야 합니다.');
|
||||
break;
|
||||
case PasswordRuleViolation.missingLowercase:
|
||||
messages.add('소문자를 최소 1자 포함해야 합니다.');
|
||||
break;
|
||||
case PasswordRuleViolation.missingDigit:
|
||||
messages.add('숫자를 최소 1자 포함해야 합니다.');
|
||||
break;
|
||||
case PasswordRuleViolation.missingSpecial:
|
||||
messages.add('특수문자를 최소 1자 포함해야 합니다.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return messages.join('\n');
|
||||
}
|
||||
|
||||
List<Widget> _buildAuditInfo(UserAccount user, ShadThemeData theme) {
|
||||
return [
|
||||
const SizedBox(height: 20),
|
||||
@@ -722,12 +922,14 @@ class _UserTable extends StatelessWidget {
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.onRestore,
|
||||
required this.onResetPassword,
|
||||
});
|
||||
|
||||
final List<UserAccount> users;
|
||||
final void Function(UserAccount user)? onEdit;
|
||||
final void Function(UserAccount user)? onDelete;
|
||||
final void Function(UserAccount user)? onRestore;
|
||||
final void Function(UserAccount user)? onResetPassword;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -761,31 +963,40 @@ class _UserTable extends StatelessWidget {
|
||||
: user.updatedAt!.toLocal().toIso8601String(),
|
||||
].map((text) => ShadTableCell(child: Text(text))).toList()..add(
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onEdit == null ? null : () => onEdit!(user),
|
||||
child: const Icon(LucideIcons.pencil, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
user.isDeleted
|
||||
? ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onRestore == null
|
||||
? null
|
||||
: () => onRestore!(user),
|
||||
child: const Icon(LucideIcons.history, size: 16),
|
||||
)
|
||||
: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onDelete == null
|
||||
? null
|
||||
: () => onDelete!(user),
|
||||
child: const Icon(LucideIcons.trash2, size: 16),
|
||||
),
|
||||
],
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onResetPassword == null
|
||||
? null
|
||||
: () => onResetPassword!(user),
|
||||
child: const Icon(LucideIcons.refreshCcw, size: 16),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onEdit == null ? null : () => onEdit!(user),
|
||||
child: const Icon(LucideIcons.pencil, size: 16),
|
||||
),
|
||||
user.isDeleted
|
||||
? ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onRestore == null
|
||||
? null
|
||||
: () => onRestore!(user),
|
||||
child: const Icon(LucideIcons.history, size: 16),
|
||||
)
|
||||
: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: onDelete == null
|
||||
? null
|
||||
: () => onDelete!(user),
|
||||
child: const Icon(LucideIcons.trash2, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user