feat(inventory): 재고 현황 요약/상세 플로우를 릴리스
- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현 - PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등) - Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트 - DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
This commit is contained in:
@@ -19,6 +19,22 @@ class PermissionSynchronizer {
|
||||
|
||||
/// 지정한 [groupId]의 메뉴 권한을 조회해 [PermissionManager]에 적용한다.
|
||||
Future<void> syncForGroup(int groupId) async {
|
||||
final permissionMap = await fetchPermissionMap(groupId);
|
||||
_manager.applyServerPermissions(permissionMap);
|
||||
}
|
||||
|
||||
/// 지정한 [groupId]의 메뉴 권한을 조회해 맵 형태로 반환한다.
|
||||
Future<Map<String, Set<PermissionAction>>> fetchPermissionMap(
|
||||
int groupId,
|
||||
) async {
|
||||
final collected = await _collectPermissions(groupId);
|
||||
if (collected.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
return buildPermissionMap(collected);
|
||||
}
|
||||
|
||||
Future<List<GroupPermission>> _collectPermissions(int groupId) async {
|
||||
final collected = <GroupPermission>[];
|
||||
var page = 1;
|
||||
|
||||
@@ -45,7 +61,6 @@ class PermissionSynchronizer {
|
||||
page += 1;
|
||||
}
|
||||
|
||||
final permissionMap = buildPermissionMap(collected);
|
||||
_manager.applyServerPermissions(permissionMap);
|
||||
return collected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,8 @@ class UserDetailDialogResult {
|
||||
}
|
||||
|
||||
typedef UserCreateCallback = Future<UserAccount?> Function(UserInput input);
|
||||
typedef UserUpdateCallback = Future<UserAccount?> Function(
|
||||
int id,
|
||||
UserInput input,
|
||||
);
|
||||
typedef UserUpdateCallback =
|
||||
Future<UserAccount?> Function(int id, UserInput input);
|
||||
typedef UserDeleteCallback = Future<bool> Function(int id);
|
||||
typedef UserRestoreCallback = Future<UserAccount?> Function(int id);
|
||||
typedef UserResetPasswordCallback = Future<UserAccount?> Function(int id);
|
||||
@@ -141,10 +139,8 @@ Future<UserDetailDialogResult?> showUserDetailDialog({
|
||||
id: _UserDetailSections.overview,
|
||||
label: '상세',
|
||||
icon: LucideIcons.info,
|
||||
builder: (_) => _UserOverviewSection(
|
||||
user: detailUser,
|
||||
dateFormat: dateFormat,
|
||||
),
|
||||
builder: (_) =>
|
||||
_UserOverviewSection(user: detailUser, dateFormat: dateFormat),
|
||||
),
|
||||
if (isDetail)
|
||||
SuperportDetailDialogSection(
|
||||
@@ -217,9 +213,7 @@ Future<UserDetailDialogResult?> showUserDetailDialog({
|
||||
),
|
||||
SuperportDetailMetadata.text(
|
||||
label: '이메일',
|
||||
value: detailUser.email?.isEmpty ?? true
|
||||
? '-'
|
||||
: detailUser.email!,
|
||||
value: detailUser.email?.isEmpty ?? true ? '-' : detailUser.email!,
|
||||
),
|
||||
SuperportDetailMetadata.text(
|
||||
label: '연락처',
|
||||
@@ -229,17 +223,13 @@ Future<UserDetailDialogResult?> showUserDetailDialog({
|
||||
),
|
||||
SuperportDetailMetadata.text(
|
||||
label: '비고',
|
||||
value: detailUser.note?.isEmpty ?? true
|
||||
? '-'
|
||||
: detailUser.note!,
|
||||
value: detailUser.note?.isEmpty ?? true ? '-' : detailUser.note!,
|
||||
),
|
||||
SuperportDetailMetadata.text(
|
||||
label: '비밀번호 변경일시',
|
||||
value: detailUser.passwordUpdatedAt == null
|
||||
? '-'
|
||||
: dateFormat.format(
|
||||
detailUser.passwordUpdatedAt!.toLocal(),
|
||||
),
|
||||
: dateFormat.format(detailUser.passwordUpdatedAt!.toLocal()),
|
||||
),
|
||||
SuperportDetailMetadata.text(
|
||||
label: '생성일시',
|
||||
@@ -285,10 +275,7 @@ class _UserDetailSections {
|
||||
|
||||
/// 사용자 주요 정보를 표시하는 섹션이다.
|
||||
class _UserOverviewSection extends StatelessWidget {
|
||||
const _UserOverviewSection({
|
||||
required this.user,
|
||||
required this.dateFormat,
|
||||
});
|
||||
const _UserOverviewSection({required this.user, required this.dateFormat});
|
||||
|
||||
final UserAccount user;
|
||||
final intl.DateFormat dateFormat;
|
||||
@@ -339,10 +326,7 @@ class _UserOverviewSection extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
rows[i].value,
|
||||
style: theme.textTheme.small,
|
||||
),
|
||||
child: Text(rows[i].value, style: theme.textTheme.small),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -426,8 +410,9 @@ class _UserSecurityContentState extends State<_UserSecurityContent> {
|
||||
label: '비밀번호 변경일시',
|
||||
value: widget.user.passwordUpdatedAt == null
|
||||
? '-'
|
||||
: widget.dateFormat
|
||||
.format(widget.user.passwordUpdatedAt!.toLocal()),
|
||||
: widget.dateFormat.format(
|
||||
widget.user.passwordUpdatedAt!.toLocal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_KeyValueColumn(
|
||||
@@ -571,8 +556,7 @@ class _UserFormState extends State<_UserForm> {
|
||||
_groupIdNotifier = ValueNotifier<int?>(user?.group?.id);
|
||||
_isActiveNotifier = ValueNotifier<bool>(user?.isActive ?? true);
|
||||
|
||||
if (_groupIdNotifier.value == null &&
|
||||
widget.groupOptions.length == 1) {
|
||||
if (_groupIdNotifier.value == null && widget.groupOptions.length == 1) {
|
||||
_groupIdNotifier.value = widget.groupOptions.first.id;
|
||||
}
|
||||
}
|
||||
@@ -613,8 +597,7 @@ class _UserFormState extends State<_UserForm> {
|
||||
}
|
||||
},
|
||||
),
|
||||
if (_employeeError != null)
|
||||
_ErrorText(_employeeError!),
|
||||
if (_employeeError != null) _ErrorText(_employeeError!),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user